whatcanGOwrong
This commit is contained in:
@@ -0,0 +1,27 @@
|
||||
Copyright (c) 2009 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
@@ -0,0 +1,22 @@
|
||||
Additional IP Rights Grant (Patents)
|
||||
|
||||
"This implementation" means the copyrightable works distributed by
|
||||
Google as part of the Go project.
|
||||
|
||||
Google hereby grants to You a perpetual, worldwide, non-exclusive,
|
||||
no-charge, royalty-free, irrevocable (except as stated in this section)
|
||||
patent license to make, have made, use, offer to sell, sell, import,
|
||||
transfer and otherwise run, modify and propagate the contents of this
|
||||
implementation of Go, where such license applies only to those patent
|
||||
claims, both currently owned or controlled by Google and acquired in
|
||||
the future, licensable by Google that are necessarily infringed by this
|
||||
implementation of Go. This grant does not include claims that would be
|
||||
infringed only as a consequence of further modification of this
|
||||
implementation. If you or your agent or exclusive licensee institute or
|
||||
order or agree to the institution of patent litigation against any
|
||||
entity (including a cross-claim or counterclaim in a lawsuit) alleging
|
||||
that this implementation of Go or any code incorporated within this
|
||||
implementation of Go constitutes direct or contributory patent
|
||||
infringement, or inducement of patent infringement, then any patent
|
||||
rights granted to you under this License for this implementation of Go
|
||||
shall terminate as of the date such litigation is filed.
|
||||
@@ -0,0 +1,17 @@
|
||||
# mod
|
||||
|
||||
[](https://pkg.go.dev/golang.org/x/mod)
|
||||
|
||||
This repository holds packages for writing tools
|
||||
that work directly with Go module mechanics.
|
||||
That is, it is for direct manipulation of Go modules themselves.
|
||||
|
||||
It is NOT about supporting general development tools that
|
||||
need to do things like load packages in module mode.
|
||||
That use case, where modules are incidental rather than the focus,
|
||||
should remain in [x/tools](https://pkg.go.dev/golang/org/x/tools),
|
||||
specifically [x/tools/go/packages](https://pkg.go.dev/golang.org/x/tools/go/packages).
|
||||
|
||||
The specific case of loading packages should still be done by
|
||||
invoking the go command, which remains the single point of
|
||||
truth for package loading algorithms.
|
||||
@@ -0,0 +1 @@
|
||||
issuerepo: golang/go
|
||||
@@ -0,0 +1,5 @@
|
||||
module golang.org/x/mod
|
||||
|
||||
go 1.18
|
||||
|
||||
require golang.org/x/tools v0.13.0 // tagx:ignore
|
||||
@@ -0,0 +1,2 @@
|
||||
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
@@ -0,0 +1,211 @@
|
||||
// Copyright 2019 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.
|
||||
|
||||
// Gosumcheck checks a go.sum file against a go.sum database server.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// gosumcheck [-h H] [-k key] [-u url] [-v] go.sum
|
||||
//
|
||||
// The -h flag changes the tile height (default 8).
|
||||
//
|
||||
// The -k flag changes the go.sum database server key.
|
||||
//
|
||||
// The -u flag overrides the URL of the server (usually set from the key name).
|
||||
//
|
||||
// The -v flag enables verbose output.
|
||||
// In particular, it causes gosumcheck to report
|
||||
// the URL and elapsed time for each server request.
|
||||
//
|
||||
// WARNING! WARNING! WARNING!
|
||||
//
|
||||
// Gosumcheck is meant as a proof of concept demo and should not be
|
||||
// used in production scripts or continuous integration testing.
|
||||
// It does not cache any downloaded information from run to run,
|
||||
// making it expensive and also keeping it from detecting server
|
||||
// misbehavior or successful HTTPS man-in-the-middle timeline forks.
|
||||
//
|
||||
// To discourage misuse in automated settings, gosumcheck does not
|
||||
// set any exit status to report whether any problems were found.
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/mod/sumdb"
|
||||
)
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stderr, "usage: gosumcheck [-h H] [-k key] [-u url] [-v] go.sum...\n")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
var (
|
||||
height = flag.Int("h", 8, "tile height")
|
||||
vkey = flag.String("k", "sum.golang.org+033de0ae+Ac4zctda0e5eza+HJyk9SxEdh+s3Ux18htTTAD8OuAn8", "key")
|
||||
url = flag.String("u", "", "url to server (overriding name)")
|
||||
vflag = flag.Bool("v", false, "enable verbose output")
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetPrefix("notecheck: ")
|
||||
log.SetFlags(0)
|
||||
|
||||
flag.Usage = usage
|
||||
flag.Parse()
|
||||
if flag.NArg() < 1 {
|
||||
usage()
|
||||
}
|
||||
|
||||
client := sumdb.NewClient(new(clientOps))
|
||||
|
||||
// Look in environment explicitly, so that if 'go env' is old and
|
||||
// doesn't know about GONOSUMDB, we at least get anything
|
||||
// set in the environment.
|
||||
env := os.Getenv("GONOSUMDB")
|
||||
if env == "" {
|
||||
out, err := exec.Command("go", "env", "GONOSUMDB").CombinedOutput()
|
||||
if err != nil {
|
||||
log.Fatalf("go env GONOSUMDB: %v\n%s", err, out)
|
||||
}
|
||||
env = strings.TrimSpace(string(out))
|
||||
}
|
||||
client.SetGONOSUMDB(env)
|
||||
|
||||
for _, arg := range flag.Args() {
|
||||
data, err := os.ReadFile(arg)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
checkGoSum(client, arg, data)
|
||||
}
|
||||
}
|
||||
|
||||
func checkGoSum(client *sumdb.Client, name string, data []byte) {
|
||||
lines := strings.Split(string(data), "\n")
|
||||
if lines[len(lines)-1] != "" {
|
||||
log.Printf("error: final line missing newline")
|
||||
return
|
||||
}
|
||||
lines = lines[:len(lines)-1]
|
||||
|
||||
errs := make([]string, len(lines))
|
||||
var wg sync.WaitGroup
|
||||
for i, line := range lines {
|
||||
wg.Add(1)
|
||||
go func(i int, line string) {
|
||||
defer wg.Done()
|
||||
f := strings.Fields(line)
|
||||
if len(f) != 3 {
|
||||
errs[i] = "invalid number of fields"
|
||||
return
|
||||
}
|
||||
|
||||
dbLines, err := client.Lookup(f[0], f[1])
|
||||
if err != nil {
|
||||
if err == sumdb.ErrGONOSUMDB {
|
||||
errs[i] = fmt.Sprintf("%s@%s: %v", f[0], f[1], err)
|
||||
} else {
|
||||
// Otherwise Lookup properly adds the prefix itself.
|
||||
errs[i] = err.Error()
|
||||
}
|
||||
return
|
||||
}
|
||||
hashAlgPrefix := f[0] + " " + f[1] + " " + f[2][:strings.Index(f[2], ":")+1]
|
||||
for _, dbLine := range dbLines {
|
||||
if dbLine == line {
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(dbLine, hashAlgPrefix) {
|
||||
errs[i] = fmt.Sprintf("%s@%s hash mismatch: have %s, want %s", f[0], f[1], line, dbLine)
|
||||
return
|
||||
}
|
||||
}
|
||||
errs[i] = fmt.Sprintf("%s@%s hash algorithm mismatch: have %s, want one of:\n\t%s", f[0], f[1], line, strings.Join(dbLines, "\n\t"))
|
||||
}(i, line)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for i, err := range errs {
|
||||
if err != "" {
|
||||
fmt.Printf("%s:%d: %s\n", name, i+1, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type clientOps struct{}
|
||||
|
||||
func (*clientOps) ReadConfig(file string) ([]byte, error) {
|
||||
if file == "key" {
|
||||
return []byte(*vkey), nil
|
||||
}
|
||||
if strings.HasSuffix(file, "/latest") {
|
||||
// Looking for cached latest tree head.
|
||||
// Empty result means empty tree.
|
||||
return []byte{}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("unknown config %s", file)
|
||||
}
|
||||
|
||||
func (*clientOps) WriteConfig(file string, old, new []byte) error {
|
||||
// Ignore writes.
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*clientOps) ReadCache(file string) ([]byte, error) {
|
||||
return nil, fmt.Errorf("no cache")
|
||||
}
|
||||
|
||||
func (*clientOps) WriteCache(file string, data []byte) {
|
||||
// Ignore writes.
|
||||
}
|
||||
|
||||
func (*clientOps) Log(msg string) {
|
||||
log.Print(msg)
|
||||
}
|
||||
|
||||
func (*clientOps) SecurityError(msg string) {
|
||||
log.Fatal(msg)
|
||||
}
|
||||
|
||||
func init() {
|
||||
http.DefaultClient.Timeout = 1 * time.Minute
|
||||
}
|
||||
|
||||
func (*clientOps) ReadRemote(path string) ([]byte, error) {
|
||||
name := *vkey
|
||||
if i := strings.Index(name, "+"); i >= 0 {
|
||||
name = name[:i]
|
||||
}
|
||||
start := time.Now()
|
||||
target := "https://" + name + path
|
||||
if *url != "" {
|
||||
target = *url + path
|
||||
}
|
||||
resp, err := http.Get(target)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("GET %v: %v", target, resp.Status)
|
||||
}
|
||||
data, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if *vflag {
|
||||
fmt.Fprintf(os.Stderr, "%.3fs %s\n", time.Since(start).Seconds(), target)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
go build -o gosumcheck.exe
|
||||
export GONOSUMDB=*/text # rsc.io/text but not golang.org/x/text
|
||||
./gosumcheck.exe "$@" -v test.sum
|
||||
rm -f ./gosumcheck.exe
|
||||
echo PASS
|
||||
@@ -0,0 +1,6 @@
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
|
||||
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
|
||||
rsc.io/text v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
@@ -0,0 +1,78 @@
|
||||
// Copyright 2018 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 lazyregexp is a thin wrapper over regexp, allowing the use of global
|
||||
// regexp variables without forcing them to be compiled at init.
|
||||
package lazyregexp
|
||||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Regexp is a wrapper around [regexp.Regexp], where the underlying regexp will be
|
||||
// compiled the first time it is needed.
|
||||
type Regexp struct {
|
||||
str string
|
||||
once sync.Once
|
||||
rx *regexp.Regexp
|
||||
}
|
||||
|
||||
func (r *Regexp) re() *regexp.Regexp {
|
||||
r.once.Do(r.build)
|
||||
return r.rx
|
||||
}
|
||||
|
||||
func (r *Regexp) build() {
|
||||
r.rx = regexp.MustCompile(r.str)
|
||||
r.str = ""
|
||||
}
|
||||
|
||||
func (r *Regexp) FindSubmatch(s []byte) [][]byte {
|
||||
return r.re().FindSubmatch(s)
|
||||
}
|
||||
|
||||
func (r *Regexp) FindStringSubmatch(s string) []string {
|
||||
return r.re().FindStringSubmatch(s)
|
||||
}
|
||||
|
||||
func (r *Regexp) FindStringSubmatchIndex(s string) []int {
|
||||
return r.re().FindStringSubmatchIndex(s)
|
||||
}
|
||||
|
||||
func (r *Regexp) ReplaceAllString(src, repl string) string {
|
||||
return r.re().ReplaceAllString(src, repl)
|
||||
}
|
||||
|
||||
func (r *Regexp) FindString(s string) string {
|
||||
return r.re().FindString(s)
|
||||
}
|
||||
|
||||
func (r *Regexp) FindAllString(s string, n int) []string {
|
||||
return r.re().FindAllString(s, n)
|
||||
}
|
||||
|
||||
func (r *Regexp) MatchString(s string) bool {
|
||||
return r.re().MatchString(s)
|
||||
}
|
||||
|
||||
func (r *Regexp) SubexpNames() []string {
|
||||
return r.re().SubexpNames()
|
||||
}
|
||||
|
||||
var inTest = len(os.Args) > 0 && strings.HasSuffix(strings.TrimSuffix(os.Args[0], ".exe"), ".test")
|
||||
|
||||
// New creates a new lazy regexp, delaying the compiling work until it is first
|
||||
// needed. If the code is being run as part of tests, the regexp compiling will
|
||||
// happen immediately.
|
||||
func New(str string) *Regexp {
|
||||
lr := &Regexp{str: str}
|
||||
if inTest {
|
||||
// In tests, always compile the regexps early.
|
||||
lr.re()
|
||||
}
|
||||
return lr
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
// Copyright 2018 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.
|
||||
|
||||
// Module file printer.
|
||||
|
||||
package modfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Format returns a go.mod file as a byte slice, formatted in standard style.
|
||||
func Format(f *FileSyntax) []byte {
|
||||
pr := &printer{}
|
||||
pr.file(f)
|
||||
|
||||
// remove trailing blank lines
|
||||
b := pr.Bytes()
|
||||
for len(b) > 0 && b[len(b)-1] == '\n' && (len(b) == 1 || b[len(b)-2] == '\n') {
|
||||
b = b[:len(b)-1]
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// A printer collects the state during printing of a file or expression.
|
||||
type printer struct {
|
||||
bytes.Buffer // output buffer
|
||||
comment []Comment // pending end-of-line comments
|
||||
margin int // left margin (indent), a number of tabs
|
||||
}
|
||||
|
||||
// printf prints to the buffer.
|
||||
func (p *printer) printf(format string, args ...interface{}) {
|
||||
fmt.Fprintf(p, format, args...)
|
||||
}
|
||||
|
||||
// indent returns the position on the current line, in bytes, 0-indexed.
|
||||
func (p *printer) indent() int {
|
||||
b := p.Bytes()
|
||||
n := 0
|
||||
for n < len(b) && b[len(b)-1-n] != '\n' {
|
||||
n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// newline ends the current line, flushing end-of-line comments.
|
||||
func (p *printer) newline() {
|
||||
if len(p.comment) > 0 {
|
||||
p.printf(" ")
|
||||
for i, com := range p.comment {
|
||||
if i > 0 {
|
||||
p.trim()
|
||||
p.printf("\n")
|
||||
for i := 0; i < p.margin; i++ {
|
||||
p.printf("\t")
|
||||
}
|
||||
}
|
||||
p.printf("%s", strings.TrimSpace(com.Token))
|
||||
}
|
||||
p.comment = p.comment[:0]
|
||||
}
|
||||
|
||||
p.trim()
|
||||
if b := p.Bytes(); len(b) == 0 || (len(b) >= 2 && b[len(b)-1] == '\n' && b[len(b)-2] == '\n') {
|
||||
// skip the blank line at top of file or after a blank line
|
||||
} else {
|
||||
p.printf("\n")
|
||||
}
|
||||
for i := 0; i < p.margin; i++ {
|
||||
p.printf("\t")
|
||||
}
|
||||
}
|
||||
|
||||
// trim removes trailing spaces and tabs from the current line.
|
||||
func (p *printer) trim() {
|
||||
// Remove trailing spaces and tabs from line we're about to end.
|
||||
b := p.Bytes()
|
||||
n := len(b)
|
||||
for n > 0 && (b[n-1] == '\t' || b[n-1] == ' ') {
|
||||
n--
|
||||
}
|
||||
p.Truncate(n)
|
||||
}
|
||||
|
||||
// file formats the given file into the print buffer.
|
||||
func (p *printer) file(f *FileSyntax) {
|
||||
for _, com := range f.Before {
|
||||
p.printf("%s", strings.TrimSpace(com.Token))
|
||||
p.newline()
|
||||
}
|
||||
|
||||
for i, stmt := range f.Stmt {
|
||||
switch x := stmt.(type) {
|
||||
case *CommentBlock:
|
||||
// comments already handled
|
||||
p.expr(x)
|
||||
|
||||
default:
|
||||
p.expr(x)
|
||||
p.newline()
|
||||
}
|
||||
|
||||
for _, com := range stmt.Comment().After {
|
||||
p.printf("%s", strings.TrimSpace(com.Token))
|
||||
p.newline()
|
||||
}
|
||||
|
||||
if i+1 < len(f.Stmt) {
|
||||
p.newline()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *printer) expr(x Expr) {
|
||||
// Emit line-comments preceding this expression.
|
||||
if before := x.Comment().Before; len(before) > 0 {
|
||||
// Want to print a line comment.
|
||||
// Line comments must be at the current margin.
|
||||
p.trim()
|
||||
if p.indent() > 0 {
|
||||
// There's other text on the line. Start a new line.
|
||||
p.printf("\n")
|
||||
}
|
||||
// Re-indent to margin.
|
||||
for i := 0; i < p.margin; i++ {
|
||||
p.printf("\t")
|
||||
}
|
||||
for _, com := range before {
|
||||
p.printf("%s", strings.TrimSpace(com.Token))
|
||||
p.newline()
|
||||
}
|
||||
}
|
||||
|
||||
switch x := x.(type) {
|
||||
default:
|
||||
panic(fmt.Errorf("printer: unexpected type %T", x))
|
||||
|
||||
case *CommentBlock:
|
||||
// done
|
||||
|
||||
case *LParen:
|
||||
p.printf("(")
|
||||
case *RParen:
|
||||
p.printf(")")
|
||||
|
||||
case *Line:
|
||||
p.tokens(x.Token)
|
||||
|
||||
case *LineBlock:
|
||||
p.tokens(x.Token)
|
||||
p.printf(" ")
|
||||
p.expr(&x.LParen)
|
||||
p.margin++
|
||||
for _, l := range x.Line {
|
||||
p.newline()
|
||||
p.expr(l)
|
||||
}
|
||||
p.margin--
|
||||
p.newline()
|
||||
p.expr(&x.RParen)
|
||||
}
|
||||
|
||||
// Queue end-of-line comments for printing when we
|
||||
// reach the end of the line.
|
||||
p.comment = append(p.comment, x.Comment().Suffix...)
|
||||
}
|
||||
|
||||
func (p *printer) tokens(tokens []string) {
|
||||
sep := ""
|
||||
for _, t := range tokens {
|
||||
if t == "," || t == ")" || t == "]" || t == "}" {
|
||||
sep = ""
|
||||
}
|
||||
p.printf("%s%s", sep, t)
|
||||
sep = " "
|
||||
if t == "(" || t == "[" || t == "{" {
|
||||
sep = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,958 @@
|
||||
// Copyright 2018 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 modfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// A Position describes an arbitrary source position in a file, including the
|
||||
// file, line, column, and byte offset.
|
||||
type Position struct {
|
||||
Line int // line in input (starting at 1)
|
||||
LineRune int // rune in line (starting at 1)
|
||||
Byte int // byte in input (starting at 0)
|
||||
}
|
||||
|
||||
// add returns the position at the end of s, assuming it starts at p.
|
||||
func (p Position) add(s string) Position {
|
||||
p.Byte += len(s)
|
||||
if n := strings.Count(s, "\n"); n > 0 {
|
||||
p.Line += n
|
||||
s = s[strings.LastIndex(s, "\n")+1:]
|
||||
p.LineRune = 1
|
||||
}
|
||||
p.LineRune += utf8.RuneCountInString(s)
|
||||
return p
|
||||
}
|
||||
|
||||
// An Expr represents an input element.
|
||||
type Expr interface {
|
||||
// Span returns the start and end position of the expression,
|
||||
// excluding leading or trailing comments.
|
||||
Span() (start, end Position)
|
||||
|
||||
// Comment returns the comments attached to the expression.
|
||||
// This method would normally be named 'Comments' but that
|
||||
// would interfere with embedding a type of the same name.
|
||||
Comment() *Comments
|
||||
}
|
||||
|
||||
// A Comment represents a single // comment.
|
||||
type Comment struct {
|
||||
Start Position
|
||||
Token string // without trailing newline
|
||||
Suffix bool // an end of line (not whole line) comment
|
||||
}
|
||||
|
||||
// Comments collects the comments associated with an expression.
|
||||
type Comments struct {
|
||||
Before []Comment // whole-line comments before this expression
|
||||
Suffix []Comment // end-of-line comments after this expression
|
||||
|
||||
// For top-level expressions only, After lists whole-line
|
||||
// comments following the expression.
|
||||
After []Comment
|
||||
}
|
||||
|
||||
// Comment returns the receiver. This isn't useful by itself, but
|
||||
// a [Comments] struct is embedded into all the expression
|
||||
// implementation types, and this gives each of those a Comment
|
||||
// method to satisfy the Expr interface.
|
||||
func (c *Comments) Comment() *Comments {
|
||||
return c
|
||||
}
|
||||
|
||||
// A FileSyntax represents an entire go.mod file.
|
||||
type FileSyntax struct {
|
||||
Name string // file path
|
||||
Comments
|
||||
Stmt []Expr
|
||||
}
|
||||
|
||||
func (x *FileSyntax) Span() (start, end Position) {
|
||||
if len(x.Stmt) == 0 {
|
||||
return
|
||||
}
|
||||
start, _ = x.Stmt[0].Span()
|
||||
_, end = x.Stmt[len(x.Stmt)-1].Span()
|
||||
return start, end
|
||||
}
|
||||
|
||||
// addLine adds a line containing the given tokens to the file.
|
||||
//
|
||||
// If the first token of the hint matches the first token of the
|
||||
// line, the new line is added at the end of the block containing hint,
|
||||
// extracting hint into a new block if it is not yet in one.
|
||||
//
|
||||
// If the hint is non-nil buts its first token does not match,
|
||||
// the new line is added after the block containing hint
|
||||
// (or hint itself, if not in a block).
|
||||
//
|
||||
// If no hint is provided, addLine appends the line to the end of
|
||||
// the last block with a matching first token,
|
||||
// or to the end of the file if no such block exists.
|
||||
func (x *FileSyntax) addLine(hint Expr, tokens ...string) *Line {
|
||||
if hint == nil {
|
||||
// If no hint given, add to the last statement of the given type.
|
||||
Loop:
|
||||
for i := len(x.Stmt) - 1; i >= 0; i-- {
|
||||
stmt := x.Stmt[i]
|
||||
switch stmt := stmt.(type) {
|
||||
case *Line:
|
||||
if stmt.Token != nil && stmt.Token[0] == tokens[0] {
|
||||
hint = stmt
|
||||
break Loop
|
||||
}
|
||||
case *LineBlock:
|
||||
if stmt.Token[0] == tokens[0] {
|
||||
hint = stmt
|
||||
break Loop
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
newLineAfter := func(i int) *Line {
|
||||
new := &Line{Token: tokens}
|
||||
if i == len(x.Stmt) {
|
||||
x.Stmt = append(x.Stmt, new)
|
||||
} else {
|
||||
x.Stmt = append(x.Stmt, nil)
|
||||
copy(x.Stmt[i+2:], x.Stmt[i+1:])
|
||||
x.Stmt[i+1] = new
|
||||
}
|
||||
return new
|
||||
}
|
||||
|
||||
if hint != nil {
|
||||
for i, stmt := range x.Stmt {
|
||||
switch stmt := stmt.(type) {
|
||||
case *Line:
|
||||
if stmt == hint {
|
||||
if stmt.Token == nil || stmt.Token[0] != tokens[0] {
|
||||
return newLineAfter(i)
|
||||
}
|
||||
|
||||
// Convert line to line block.
|
||||
stmt.InBlock = true
|
||||
block := &LineBlock{Token: stmt.Token[:1], Line: []*Line{stmt}}
|
||||
stmt.Token = stmt.Token[1:]
|
||||
x.Stmt[i] = block
|
||||
new := &Line{Token: tokens[1:], InBlock: true}
|
||||
block.Line = append(block.Line, new)
|
||||
return new
|
||||
}
|
||||
|
||||
case *LineBlock:
|
||||
if stmt == hint {
|
||||
if stmt.Token[0] != tokens[0] {
|
||||
return newLineAfter(i)
|
||||
}
|
||||
|
||||
new := &Line{Token: tokens[1:], InBlock: true}
|
||||
stmt.Line = append(stmt.Line, new)
|
||||
return new
|
||||
}
|
||||
|
||||
for j, line := range stmt.Line {
|
||||
if line == hint {
|
||||
if stmt.Token[0] != tokens[0] {
|
||||
return newLineAfter(i)
|
||||
}
|
||||
|
||||
// Add new line after hint within the block.
|
||||
stmt.Line = append(stmt.Line, nil)
|
||||
copy(stmt.Line[j+2:], stmt.Line[j+1:])
|
||||
new := &Line{Token: tokens[1:], InBlock: true}
|
||||
stmt.Line[j+1] = new
|
||||
return new
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new := &Line{Token: tokens}
|
||||
x.Stmt = append(x.Stmt, new)
|
||||
return new
|
||||
}
|
||||
|
||||
func (x *FileSyntax) updateLine(line *Line, tokens ...string) {
|
||||
if line.InBlock {
|
||||
tokens = tokens[1:]
|
||||
}
|
||||
line.Token = tokens
|
||||
}
|
||||
|
||||
// markRemoved modifies line so that it (and its end-of-line comment, if any)
|
||||
// will be dropped by (*FileSyntax).Cleanup.
|
||||
func (line *Line) markRemoved() {
|
||||
line.Token = nil
|
||||
line.Comments.Suffix = nil
|
||||
}
|
||||
|
||||
// Cleanup cleans up the file syntax x after any edit operations.
|
||||
// To avoid quadratic behavior, (*Line).markRemoved marks the line as dead
|
||||
// by setting line.Token = nil but does not remove it from the slice
|
||||
// in which it appears. After edits have all been indicated,
|
||||
// calling Cleanup cleans out the dead lines.
|
||||
func (x *FileSyntax) Cleanup() {
|
||||
w := 0
|
||||
for _, stmt := range x.Stmt {
|
||||
switch stmt := stmt.(type) {
|
||||
case *Line:
|
||||
if stmt.Token == nil {
|
||||
continue
|
||||
}
|
||||
case *LineBlock:
|
||||
ww := 0
|
||||
for _, line := range stmt.Line {
|
||||
if line.Token != nil {
|
||||
stmt.Line[ww] = line
|
||||
ww++
|
||||
}
|
||||
}
|
||||
if ww == 0 {
|
||||
continue
|
||||
}
|
||||
if ww == 1 {
|
||||
// Collapse block into single line.
|
||||
line := &Line{
|
||||
Comments: Comments{
|
||||
Before: commentsAdd(stmt.Before, stmt.Line[0].Before),
|
||||
Suffix: commentsAdd(stmt.Line[0].Suffix, stmt.Suffix),
|
||||
After: commentsAdd(stmt.Line[0].After, stmt.After),
|
||||
},
|
||||
Token: stringsAdd(stmt.Token, stmt.Line[0].Token),
|
||||
}
|
||||
x.Stmt[w] = line
|
||||
w++
|
||||
continue
|
||||
}
|
||||
stmt.Line = stmt.Line[:ww]
|
||||
}
|
||||
x.Stmt[w] = stmt
|
||||
w++
|
||||
}
|
||||
x.Stmt = x.Stmt[:w]
|
||||
}
|
||||
|
||||
func commentsAdd(x, y []Comment) []Comment {
|
||||
return append(x[:len(x):len(x)], y...)
|
||||
}
|
||||
|
||||
func stringsAdd(x, y []string) []string {
|
||||
return append(x[:len(x):len(x)], y...)
|
||||
}
|
||||
|
||||
// A CommentBlock represents a top-level block of comments separate
|
||||
// from any rule.
|
||||
type CommentBlock struct {
|
||||
Comments
|
||||
Start Position
|
||||
}
|
||||
|
||||
func (x *CommentBlock) Span() (start, end Position) {
|
||||
return x.Start, x.Start
|
||||
}
|
||||
|
||||
// A Line is a single line of tokens.
|
||||
type Line struct {
|
||||
Comments
|
||||
Start Position
|
||||
Token []string
|
||||
InBlock bool
|
||||
End Position
|
||||
}
|
||||
|
||||
func (x *Line) Span() (start, end Position) {
|
||||
return x.Start, x.End
|
||||
}
|
||||
|
||||
// A LineBlock is a factored block of lines, like
|
||||
//
|
||||
// require (
|
||||
// "x"
|
||||
// "y"
|
||||
// )
|
||||
type LineBlock struct {
|
||||
Comments
|
||||
Start Position
|
||||
LParen LParen
|
||||
Token []string
|
||||
Line []*Line
|
||||
RParen RParen
|
||||
}
|
||||
|
||||
func (x *LineBlock) Span() (start, end Position) {
|
||||
return x.Start, x.RParen.Pos.add(")")
|
||||
}
|
||||
|
||||
// An LParen represents the beginning of a parenthesized line block.
|
||||
// It is a place to store suffix comments.
|
||||
type LParen struct {
|
||||
Comments
|
||||
Pos Position
|
||||
}
|
||||
|
||||
func (x *LParen) Span() (start, end Position) {
|
||||
return x.Pos, x.Pos.add(")")
|
||||
}
|
||||
|
||||
// An RParen represents the end of a parenthesized line block.
|
||||
// It is a place to store whole-line (before) comments.
|
||||
type RParen struct {
|
||||
Comments
|
||||
Pos Position
|
||||
}
|
||||
|
||||
func (x *RParen) Span() (start, end Position) {
|
||||
return x.Pos, x.Pos.add(")")
|
||||
}
|
||||
|
||||
// An input represents a single input file being parsed.
|
||||
type input struct {
|
||||
// Lexing state.
|
||||
filename string // name of input file, for errors
|
||||
complete []byte // entire input
|
||||
remaining []byte // remaining input
|
||||
tokenStart []byte // token being scanned to end of input
|
||||
token token // next token to be returned by lex, peek
|
||||
pos Position // current input position
|
||||
comments []Comment // accumulated comments
|
||||
|
||||
// Parser state.
|
||||
file *FileSyntax // returned top-level syntax tree
|
||||
parseErrors ErrorList // errors encountered during parsing
|
||||
|
||||
// Comment assignment state.
|
||||
pre []Expr // all expressions, in preorder traversal
|
||||
post []Expr // all expressions, in postorder traversal
|
||||
}
|
||||
|
||||
func newInput(filename string, data []byte) *input {
|
||||
return &input{
|
||||
filename: filename,
|
||||
complete: data,
|
||||
remaining: data,
|
||||
pos: Position{Line: 1, LineRune: 1, Byte: 0},
|
||||
}
|
||||
}
|
||||
|
||||
// parse parses the input file.
|
||||
func parse(file string, data []byte) (f *FileSyntax, err error) {
|
||||
// The parser panics for both routine errors like syntax errors
|
||||
// and for programmer bugs like array index errors.
|
||||
// Turn both into error returns. Catching bug panics is
|
||||
// especially important when processing many files.
|
||||
in := newInput(file, data)
|
||||
defer func() {
|
||||
if e := recover(); e != nil && e != &in.parseErrors {
|
||||
in.parseErrors = append(in.parseErrors, Error{
|
||||
Filename: in.filename,
|
||||
Pos: in.pos,
|
||||
Err: fmt.Errorf("internal error: %v", e),
|
||||
})
|
||||
}
|
||||
if err == nil && len(in.parseErrors) > 0 {
|
||||
err = in.parseErrors
|
||||
}
|
||||
}()
|
||||
|
||||
// Prime the lexer by reading in the first token. It will be available
|
||||
// in the next peek() or lex() call.
|
||||
in.readToken()
|
||||
|
||||
// Invoke the parser.
|
||||
in.parseFile()
|
||||
if len(in.parseErrors) > 0 {
|
||||
return nil, in.parseErrors
|
||||
}
|
||||
in.file.Name = in.filename
|
||||
|
||||
// Assign comments to nearby syntax.
|
||||
in.assignComments()
|
||||
|
||||
return in.file, nil
|
||||
}
|
||||
|
||||
// Error is called to report an error.
|
||||
// Error does not return: it panics.
|
||||
func (in *input) Error(s string) {
|
||||
in.parseErrors = append(in.parseErrors, Error{
|
||||
Filename: in.filename,
|
||||
Pos: in.pos,
|
||||
Err: errors.New(s),
|
||||
})
|
||||
panic(&in.parseErrors)
|
||||
}
|
||||
|
||||
// eof reports whether the input has reached end of file.
|
||||
func (in *input) eof() bool {
|
||||
return len(in.remaining) == 0
|
||||
}
|
||||
|
||||
// peekRune returns the next rune in the input without consuming it.
|
||||
func (in *input) peekRune() int {
|
||||
if len(in.remaining) == 0 {
|
||||
return 0
|
||||
}
|
||||
r, _ := utf8.DecodeRune(in.remaining)
|
||||
return int(r)
|
||||
}
|
||||
|
||||
// peekPrefix reports whether the remaining input begins with the given prefix.
|
||||
func (in *input) peekPrefix(prefix string) bool {
|
||||
// This is like bytes.HasPrefix(in.remaining, []byte(prefix))
|
||||
// but without the allocation of the []byte copy of prefix.
|
||||
for i := 0; i < len(prefix); i++ {
|
||||
if i >= len(in.remaining) || in.remaining[i] != prefix[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// readRune consumes and returns the next rune in the input.
|
||||
func (in *input) readRune() int {
|
||||
if len(in.remaining) == 0 {
|
||||
in.Error("internal lexer error: readRune at EOF")
|
||||
}
|
||||
r, size := utf8.DecodeRune(in.remaining)
|
||||
in.remaining = in.remaining[size:]
|
||||
if r == '\n' {
|
||||
in.pos.Line++
|
||||
in.pos.LineRune = 1
|
||||
} else {
|
||||
in.pos.LineRune++
|
||||
}
|
||||
in.pos.Byte += size
|
||||
return int(r)
|
||||
}
|
||||
|
||||
type token struct {
|
||||
kind tokenKind
|
||||
pos Position
|
||||
endPos Position
|
||||
text string
|
||||
}
|
||||
|
||||
type tokenKind int
|
||||
|
||||
const (
|
||||
_EOF tokenKind = -(iota + 1)
|
||||
_EOLCOMMENT
|
||||
_IDENT
|
||||
_STRING
|
||||
_COMMENT
|
||||
|
||||
// newlines and punctuation tokens are allowed as ASCII codes.
|
||||
)
|
||||
|
||||
func (k tokenKind) isComment() bool {
|
||||
return k == _COMMENT || k == _EOLCOMMENT
|
||||
}
|
||||
|
||||
// isEOL returns whether a token terminates a line.
|
||||
func (k tokenKind) isEOL() bool {
|
||||
return k == _EOF || k == _EOLCOMMENT || k == '\n'
|
||||
}
|
||||
|
||||
// startToken marks the beginning of the next input token.
|
||||
// It must be followed by a call to endToken, once the token's text has
|
||||
// been consumed using readRune.
|
||||
func (in *input) startToken() {
|
||||
in.tokenStart = in.remaining
|
||||
in.token.text = ""
|
||||
in.token.pos = in.pos
|
||||
}
|
||||
|
||||
// endToken marks the end of an input token.
|
||||
// It records the actual token string in tok.text.
|
||||
// A single trailing newline (LF or CRLF) will be removed from comment tokens.
|
||||
func (in *input) endToken(kind tokenKind) {
|
||||
in.token.kind = kind
|
||||
text := string(in.tokenStart[:len(in.tokenStart)-len(in.remaining)])
|
||||
if kind.isComment() {
|
||||
if strings.HasSuffix(text, "\r\n") {
|
||||
text = text[:len(text)-2]
|
||||
} else {
|
||||
text = strings.TrimSuffix(text, "\n")
|
||||
}
|
||||
}
|
||||
in.token.text = text
|
||||
in.token.endPos = in.pos
|
||||
}
|
||||
|
||||
// peek returns the kind of the next token returned by lex.
|
||||
func (in *input) peek() tokenKind {
|
||||
return in.token.kind
|
||||
}
|
||||
|
||||
// lex is called from the parser to obtain the next input token.
|
||||
func (in *input) lex() token {
|
||||
tok := in.token
|
||||
in.readToken()
|
||||
return tok
|
||||
}
|
||||
|
||||
// readToken lexes the next token from the text and stores it in in.token.
|
||||
func (in *input) readToken() {
|
||||
// Skip past spaces, stopping at non-space or EOF.
|
||||
for !in.eof() {
|
||||
c := in.peekRune()
|
||||
if c == ' ' || c == '\t' || c == '\r' {
|
||||
in.readRune()
|
||||
continue
|
||||
}
|
||||
|
||||
// Comment runs to end of line.
|
||||
if in.peekPrefix("//") {
|
||||
in.startToken()
|
||||
|
||||
// Is this comment the only thing on its line?
|
||||
// Find the last \n before this // and see if it's all
|
||||
// spaces from there to here.
|
||||
i := bytes.LastIndex(in.complete[:in.pos.Byte], []byte("\n"))
|
||||
suffix := len(bytes.TrimSpace(in.complete[i+1:in.pos.Byte])) > 0
|
||||
in.readRune()
|
||||
in.readRune()
|
||||
|
||||
// Consume comment.
|
||||
for len(in.remaining) > 0 && in.readRune() != '\n' {
|
||||
}
|
||||
|
||||
// If we are at top level (not in a statement), hand the comment to
|
||||
// the parser as a _COMMENT token. The grammar is written
|
||||
// to handle top-level comments itself.
|
||||
if !suffix {
|
||||
in.endToken(_COMMENT)
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, save comment for later attachment to syntax tree.
|
||||
in.endToken(_EOLCOMMENT)
|
||||
in.comments = append(in.comments, Comment{in.token.pos, in.token.text, suffix})
|
||||
return
|
||||
}
|
||||
|
||||
if in.peekPrefix("/*") {
|
||||
in.Error("mod files must use // comments (not /* */ comments)")
|
||||
}
|
||||
|
||||
// Found non-space non-comment.
|
||||
break
|
||||
}
|
||||
|
||||
// Found the beginning of the next token.
|
||||
in.startToken()
|
||||
|
||||
// End of file.
|
||||
if in.eof() {
|
||||
in.endToken(_EOF)
|
||||
return
|
||||
}
|
||||
|
||||
// Punctuation tokens.
|
||||
switch c := in.peekRune(); c {
|
||||
case '\n', '(', ')', '[', ']', '{', '}', ',':
|
||||
in.readRune()
|
||||
in.endToken(tokenKind(c))
|
||||
return
|
||||
|
||||
case '"', '`': // quoted string
|
||||
quote := c
|
||||
in.readRune()
|
||||
for {
|
||||
if in.eof() {
|
||||
in.pos = in.token.pos
|
||||
in.Error("unexpected EOF in string")
|
||||
}
|
||||
if in.peekRune() == '\n' {
|
||||
in.Error("unexpected newline in string")
|
||||
}
|
||||
c := in.readRune()
|
||||
if c == quote {
|
||||
break
|
||||
}
|
||||
if c == '\\' && quote != '`' {
|
||||
if in.eof() {
|
||||
in.pos = in.token.pos
|
||||
in.Error("unexpected EOF in string")
|
||||
}
|
||||
in.readRune()
|
||||
}
|
||||
}
|
||||
in.endToken(_STRING)
|
||||
return
|
||||
}
|
||||
|
||||
// Checked all punctuation. Must be identifier token.
|
||||
if c := in.peekRune(); !isIdent(c) {
|
||||
in.Error(fmt.Sprintf("unexpected input character %#q", c))
|
||||
}
|
||||
|
||||
// Scan over identifier.
|
||||
for isIdent(in.peekRune()) {
|
||||
if in.peekPrefix("//") {
|
||||
break
|
||||
}
|
||||
if in.peekPrefix("/*") {
|
||||
in.Error("mod files must use // comments (not /* */ comments)")
|
||||
}
|
||||
in.readRune()
|
||||
}
|
||||
in.endToken(_IDENT)
|
||||
}
|
||||
|
||||
// isIdent reports whether c is an identifier rune.
|
||||
// We treat most printable runes as identifier runes, except for a handful of
|
||||
// ASCII punctuation characters.
|
||||
func isIdent(c int) bool {
|
||||
switch r := rune(c); r {
|
||||
case ' ', '(', ')', '[', ']', '{', '}', ',':
|
||||
return false
|
||||
default:
|
||||
return !unicode.IsSpace(r) && unicode.IsPrint(r)
|
||||
}
|
||||
}
|
||||
|
||||
// Comment assignment.
|
||||
// We build two lists of all subexpressions, preorder and postorder.
|
||||
// The preorder list is ordered by start location, with outer expressions first.
|
||||
// The postorder list is ordered by end location, with outer expressions last.
|
||||
// We use the preorder list to assign each whole-line comment to the syntax
|
||||
// immediately following it, and we use the postorder list to assign each
|
||||
// end-of-line comment to the syntax immediately preceding it.
|
||||
|
||||
// order walks the expression adding it and its subexpressions to the
|
||||
// preorder and postorder lists.
|
||||
func (in *input) order(x Expr) {
|
||||
if x != nil {
|
||||
in.pre = append(in.pre, x)
|
||||
}
|
||||
switch x := x.(type) {
|
||||
default:
|
||||
panic(fmt.Errorf("order: unexpected type %T", x))
|
||||
case nil:
|
||||
// nothing
|
||||
case *LParen, *RParen:
|
||||
// nothing
|
||||
case *CommentBlock:
|
||||
// nothing
|
||||
case *Line:
|
||||
// nothing
|
||||
case *FileSyntax:
|
||||
for _, stmt := range x.Stmt {
|
||||
in.order(stmt)
|
||||
}
|
||||
case *LineBlock:
|
||||
in.order(&x.LParen)
|
||||
for _, l := range x.Line {
|
||||
in.order(l)
|
||||
}
|
||||
in.order(&x.RParen)
|
||||
}
|
||||
if x != nil {
|
||||
in.post = append(in.post, x)
|
||||
}
|
||||
}
|
||||
|
||||
// assignComments attaches comments to nearby syntax.
|
||||
func (in *input) assignComments() {
|
||||
const debug = false
|
||||
|
||||
// Generate preorder and postorder lists.
|
||||
in.order(in.file)
|
||||
|
||||
// Split into whole-line comments and suffix comments.
|
||||
var line, suffix []Comment
|
||||
for _, com := range in.comments {
|
||||
if com.Suffix {
|
||||
suffix = append(suffix, com)
|
||||
} else {
|
||||
line = append(line, com)
|
||||
}
|
||||
}
|
||||
|
||||
if debug {
|
||||
for _, c := range line {
|
||||
fmt.Fprintf(os.Stderr, "LINE %q :%d:%d #%d\n", c.Token, c.Start.Line, c.Start.LineRune, c.Start.Byte)
|
||||
}
|
||||
}
|
||||
|
||||
// Assign line comments to syntax immediately following.
|
||||
for _, x := range in.pre {
|
||||
start, _ := x.Span()
|
||||
if debug {
|
||||
fmt.Fprintf(os.Stderr, "pre %T :%d:%d #%d\n", x, start.Line, start.LineRune, start.Byte)
|
||||
}
|
||||
xcom := x.Comment()
|
||||
for len(line) > 0 && start.Byte >= line[0].Start.Byte {
|
||||
if debug {
|
||||
fmt.Fprintf(os.Stderr, "ASSIGN LINE %q #%d\n", line[0].Token, line[0].Start.Byte)
|
||||
}
|
||||
xcom.Before = append(xcom.Before, line[0])
|
||||
line = line[1:]
|
||||
}
|
||||
}
|
||||
|
||||
// Remaining line comments go at end of file.
|
||||
in.file.After = append(in.file.After, line...)
|
||||
|
||||
if debug {
|
||||
for _, c := range suffix {
|
||||
fmt.Fprintf(os.Stderr, "SUFFIX %q :%d:%d #%d\n", c.Token, c.Start.Line, c.Start.LineRune, c.Start.Byte)
|
||||
}
|
||||
}
|
||||
|
||||
// Assign suffix comments to syntax immediately before.
|
||||
for i := len(in.post) - 1; i >= 0; i-- {
|
||||
x := in.post[i]
|
||||
|
||||
start, end := x.Span()
|
||||
if debug {
|
||||
fmt.Fprintf(os.Stderr, "post %T :%d:%d #%d :%d:%d #%d\n", x, start.Line, start.LineRune, start.Byte, end.Line, end.LineRune, end.Byte)
|
||||
}
|
||||
|
||||
// Do not assign suffix comments to end of line block or whole file.
|
||||
// Instead assign them to the last element inside.
|
||||
switch x.(type) {
|
||||
case *FileSyntax:
|
||||
continue
|
||||
}
|
||||
|
||||
// Do not assign suffix comments to something that starts
|
||||
// on an earlier line, so that in
|
||||
//
|
||||
// x ( y
|
||||
// z ) // comment
|
||||
//
|
||||
// we assign the comment to z and not to x ( ... ).
|
||||
if start.Line != end.Line {
|
||||
continue
|
||||
}
|
||||
xcom := x.Comment()
|
||||
for len(suffix) > 0 && end.Byte <= suffix[len(suffix)-1].Start.Byte {
|
||||
if debug {
|
||||
fmt.Fprintf(os.Stderr, "ASSIGN SUFFIX %q #%d\n", suffix[len(suffix)-1].Token, suffix[len(suffix)-1].Start.Byte)
|
||||
}
|
||||
xcom.Suffix = append(xcom.Suffix, suffix[len(suffix)-1])
|
||||
suffix = suffix[:len(suffix)-1]
|
||||
}
|
||||
}
|
||||
|
||||
// We assigned suffix comments in reverse.
|
||||
// If multiple suffix comments were appended to the same
|
||||
// expression node, they are now in reverse. Fix that.
|
||||
for _, x := range in.post {
|
||||
reverseComments(x.Comment().Suffix)
|
||||
}
|
||||
|
||||
// Remaining suffix comments go at beginning of file.
|
||||
in.file.Before = append(in.file.Before, suffix...)
|
||||
}
|
||||
|
||||
// reverseComments reverses the []Comment list.
|
||||
func reverseComments(list []Comment) {
|
||||
for i, j := 0, len(list)-1; i < j; i, j = i+1, j-1 {
|
||||
list[i], list[j] = list[j], list[i]
|
||||
}
|
||||
}
|
||||
|
||||
func (in *input) parseFile() {
|
||||
in.file = new(FileSyntax)
|
||||
var cb *CommentBlock
|
||||
for {
|
||||
switch in.peek() {
|
||||
case '\n':
|
||||
in.lex()
|
||||
if cb != nil {
|
||||
in.file.Stmt = append(in.file.Stmt, cb)
|
||||
cb = nil
|
||||
}
|
||||
case _COMMENT:
|
||||
tok := in.lex()
|
||||
if cb == nil {
|
||||
cb = &CommentBlock{Start: tok.pos}
|
||||
}
|
||||
com := cb.Comment()
|
||||
com.Before = append(com.Before, Comment{Start: tok.pos, Token: tok.text})
|
||||
case _EOF:
|
||||
if cb != nil {
|
||||
in.file.Stmt = append(in.file.Stmt, cb)
|
||||
}
|
||||
return
|
||||
default:
|
||||
in.parseStmt()
|
||||
if cb != nil {
|
||||
in.file.Stmt[len(in.file.Stmt)-1].Comment().Before = cb.Before
|
||||
cb = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (in *input) parseStmt() {
|
||||
tok := in.lex()
|
||||
start := tok.pos
|
||||
end := tok.endPos
|
||||
tokens := []string{tok.text}
|
||||
for {
|
||||
tok := in.lex()
|
||||
switch {
|
||||
case tok.kind.isEOL():
|
||||
in.file.Stmt = append(in.file.Stmt, &Line{
|
||||
Start: start,
|
||||
Token: tokens,
|
||||
End: end,
|
||||
})
|
||||
return
|
||||
|
||||
case tok.kind == '(':
|
||||
if next := in.peek(); next.isEOL() {
|
||||
// Start of block: no more tokens on this line.
|
||||
in.file.Stmt = append(in.file.Stmt, in.parseLineBlock(start, tokens, tok))
|
||||
return
|
||||
} else if next == ')' {
|
||||
rparen := in.lex()
|
||||
if in.peek().isEOL() {
|
||||
// Empty block.
|
||||
in.lex()
|
||||
in.file.Stmt = append(in.file.Stmt, &LineBlock{
|
||||
Start: start,
|
||||
Token: tokens,
|
||||
LParen: LParen{Pos: tok.pos},
|
||||
RParen: RParen{Pos: rparen.pos},
|
||||
})
|
||||
return
|
||||
}
|
||||
// '( )' in the middle of the line, not a block.
|
||||
tokens = append(tokens, tok.text, rparen.text)
|
||||
} else {
|
||||
// '(' in the middle of the line, not a block.
|
||||
tokens = append(tokens, tok.text)
|
||||
}
|
||||
|
||||
default:
|
||||
tokens = append(tokens, tok.text)
|
||||
end = tok.endPos
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (in *input) parseLineBlock(start Position, token []string, lparen token) *LineBlock {
|
||||
x := &LineBlock{
|
||||
Start: start,
|
||||
Token: token,
|
||||
LParen: LParen{Pos: lparen.pos},
|
||||
}
|
||||
var comments []Comment
|
||||
for {
|
||||
switch in.peek() {
|
||||
case _EOLCOMMENT:
|
||||
// Suffix comment, will be attached later by assignComments.
|
||||
in.lex()
|
||||
case '\n':
|
||||
// Blank line. Add an empty comment to preserve it.
|
||||
in.lex()
|
||||
if len(comments) == 0 && len(x.Line) > 0 || len(comments) > 0 && comments[len(comments)-1].Token != "" {
|
||||
comments = append(comments, Comment{})
|
||||
}
|
||||
case _COMMENT:
|
||||
tok := in.lex()
|
||||
comments = append(comments, Comment{Start: tok.pos, Token: tok.text})
|
||||
case _EOF:
|
||||
in.Error(fmt.Sprintf("syntax error (unterminated block started at %s:%d:%d)", in.filename, x.Start.Line, x.Start.LineRune))
|
||||
case ')':
|
||||
rparen := in.lex()
|
||||
x.RParen.Before = comments
|
||||
x.RParen.Pos = rparen.pos
|
||||
if !in.peek().isEOL() {
|
||||
in.Error("syntax error (expected newline after closing paren)")
|
||||
}
|
||||
in.lex()
|
||||
return x
|
||||
default:
|
||||
l := in.parseLine()
|
||||
x.Line = append(x.Line, l)
|
||||
l.Comment().Before = comments
|
||||
comments = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (in *input) parseLine() *Line {
|
||||
tok := in.lex()
|
||||
if tok.kind.isEOL() {
|
||||
in.Error("internal parse error: parseLine at end of line")
|
||||
}
|
||||
start := tok.pos
|
||||
end := tok.endPos
|
||||
tokens := []string{tok.text}
|
||||
for {
|
||||
tok := in.lex()
|
||||
if tok.kind.isEOL() {
|
||||
return &Line{
|
||||
Start: start,
|
||||
Token: tokens,
|
||||
End: end,
|
||||
InBlock: true,
|
||||
}
|
||||
}
|
||||
tokens = append(tokens, tok.text)
|
||||
end = tok.endPos
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
slashSlash = []byte("//")
|
||||
moduleStr = []byte("module")
|
||||
)
|
||||
|
||||
// ModulePath returns the module path from the gomod file text.
|
||||
// If it cannot find a module path, it returns an empty string.
|
||||
// It is tolerant of unrelated problems in the go.mod file.
|
||||
func ModulePath(mod []byte) string {
|
||||
for len(mod) > 0 {
|
||||
line := mod
|
||||
mod = nil
|
||||
if i := bytes.IndexByte(line, '\n'); i >= 0 {
|
||||
line, mod = line[:i], line[i+1:]
|
||||
}
|
||||
if i := bytes.Index(line, slashSlash); i >= 0 {
|
||||
line = line[:i]
|
||||
}
|
||||
line = bytes.TrimSpace(line)
|
||||
if !bytes.HasPrefix(line, moduleStr) {
|
||||
continue
|
||||
}
|
||||
line = line[len(moduleStr):]
|
||||
n := len(line)
|
||||
line = bytes.TrimSpace(line)
|
||||
if len(line) == n || len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
if line[0] == '"' || line[0] == '`' {
|
||||
p, err := strconv.Unquote(string(line))
|
||||
if err != nil {
|
||||
return "" // malformed quoted string or multiline module path
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
return string(line)
|
||||
}
|
||||
return "" // missing module path
|
||||
}
|
||||
@@ -0,0 +1,605 @@
|
||||
// Copyright 2018 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 modfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// exists reports whether the named file exists.
|
||||
func exists(name string) bool {
|
||||
_, err := os.Stat(name)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Test that reading and then writing the golden files
|
||||
// does not change their output.
|
||||
func TestPrintGolden(t *testing.T) {
|
||||
outs, err := filepath.Glob("testdata/*.golden")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, out := range outs {
|
||||
out := out
|
||||
name := strings.TrimSuffix(filepath.Base(out), ".golden")
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testPrint(t, out, out)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// testPrint is a helper for testing the printer.
|
||||
// It reads the file named in, reformats it, and compares
|
||||
// the result to the file named out.
|
||||
func testPrint(t *testing.T, in, out string) {
|
||||
data, err := os.ReadFile(in)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
golden, err := os.ReadFile(out)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
base := "testdata/" + filepath.Base(in)
|
||||
f, err := parse(in, data)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
ndata := Format(f)
|
||||
|
||||
if !bytes.Equal(ndata, golden) {
|
||||
t.Errorf("formatted %s incorrectly: diff shows -golden, +ours", base)
|
||||
tdiff(t, string(golden), string(ndata))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TestParsePunctuation verifies that certain ASCII punctuation characters
|
||||
// (brackets, commas) are lexed as separate tokens, even when they're
|
||||
// surrounded by identifier characters.
|
||||
func TestParsePunctuation(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
desc, src, want string
|
||||
}{
|
||||
{"paren", "require ()", "require ( )"},
|
||||
{"brackets", "require []{},", "require [ ] { } ,"},
|
||||
{"mix", "require a[b]c{d}e,", "require a [ b ] c { d } e ,"},
|
||||
{"block_mix", "require (\n\ta[b]\n)", "require ( a [ b ] )"},
|
||||
{"interval", "require [v1.0.0, v1.1.0)", "require [ v1.0.0 , v1.1.0 )"},
|
||||
} {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
f, err := parse("go.mod", []byte(test.src))
|
||||
if err != nil {
|
||||
t.Fatalf("parsing %q: %v", test.src, err)
|
||||
}
|
||||
var tokens []string
|
||||
for _, stmt := range f.Stmt {
|
||||
switch stmt := stmt.(type) {
|
||||
case *Line:
|
||||
tokens = append(tokens, stmt.Token...)
|
||||
case *LineBlock:
|
||||
tokens = append(tokens, stmt.Token...)
|
||||
tokens = append(tokens, "(")
|
||||
for _, line := range stmt.Line {
|
||||
tokens = append(tokens, line.Token...)
|
||||
}
|
||||
tokens = append(tokens, ")")
|
||||
default:
|
||||
t.Fatalf("parsing %q: unexpected statement of type %T", test.src, stmt)
|
||||
}
|
||||
}
|
||||
got := strings.Join(tokens, " ")
|
||||
if got != test.want {
|
||||
t.Errorf("parsing %q: got %q, want %q", test.src, got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseLax(t *testing.T) {
|
||||
badFile := []byte(`module m
|
||||
surprise attack
|
||||
x y (
|
||||
z
|
||||
)
|
||||
exclude v1.2.3
|
||||
replace <-!!!
|
||||
retract v1.2.3 v1.2.4
|
||||
retract (v1.2.3, v1.2.4]
|
||||
retract v1.2.3 (
|
||||
key1 value1
|
||||
key2 value2
|
||||
)
|
||||
require good v1.0.0
|
||||
`)
|
||||
f, err := ParseLax("file", badFile, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("ParseLax did not ignore irrelevant errors: %v", err)
|
||||
}
|
||||
if f.Module == nil || f.Module.Mod.Path != "m" {
|
||||
t.Errorf("module directive was not parsed")
|
||||
}
|
||||
if len(f.Require) != 1 || f.Require[0].Mod.Path != "good" {
|
||||
t.Errorf("require directive at end of file was not parsed")
|
||||
}
|
||||
}
|
||||
|
||||
// Test that when files in the testdata directory are parsed
|
||||
// and printed and parsed again, we get the same parse tree
|
||||
// both times.
|
||||
func TestPrintParse(t *testing.T) {
|
||||
outs, err := filepath.Glob("testdata/*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, out := range outs {
|
||||
out := out
|
||||
name := filepath.Base(out)
|
||||
if !strings.HasSuffix(out, ".in") && !strings.HasSuffix(out, ".golden") {
|
||||
continue
|
||||
}
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
data, err := os.ReadFile(out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
base := "testdata/" + filepath.Base(out)
|
||||
f, err := parse(base, data)
|
||||
if err != nil {
|
||||
t.Fatalf("parsing original: %v", err)
|
||||
}
|
||||
|
||||
ndata := Format(f)
|
||||
f2, err := parse(base, ndata)
|
||||
if err != nil {
|
||||
t.Fatalf("parsing reformatted: %v", err)
|
||||
}
|
||||
|
||||
eq := eqchecker{file: base}
|
||||
if err := eq.check(f, f2); err != nil {
|
||||
t.Errorf("not equal (parse/Format/parse): %v", err)
|
||||
}
|
||||
|
||||
pf1, err := Parse(base, data, nil)
|
||||
if err != nil {
|
||||
switch base {
|
||||
case "testdata/block.golden",
|
||||
"testdata/block.in",
|
||||
"testdata/comment.golden",
|
||||
"testdata/comment.in",
|
||||
"testdata/rule1.golden":
|
||||
// ignore
|
||||
default:
|
||||
t.Errorf("should parse %v: %v", base, err)
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
pf2, err := Parse(base, ndata, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Parsing reformatted: %v", err)
|
||||
}
|
||||
eq := eqchecker{file: base}
|
||||
if err := eq.check(pf1, pf2); err != nil {
|
||||
t.Errorf("not equal (parse/Format/Parse): %v", err)
|
||||
}
|
||||
|
||||
ndata2, err := pf1.Format()
|
||||
if err != nil {
|
||||
t.Errorf("reformat: %v", err)
|
||||
}
|
||||
pf3, err := Parse(base, ndata2, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Parsing reformatted2: %v", err)
|
||||
}
|
||||
eq = eqchecker{file: base}
|
||||
if err := eq.check(pf1, pf3); err != nil {
|
||||
t.Errorf("not equal (Parse/Format/Parse): %v", err)
|
||||
}
|
||||
ndata = ndata2
|
||||
}
|
||||
|
||||
if strings.HasSuffix(out, ".in") {
|
||||
golden, err := os.ReadFile(strings.TrimSuffix(out, ".in") + ".golden")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(ndata, golden) {
|
||||
t.Errorf("formatted %s incorrectly: diff shows -golden, +ours", base)
|
||||
tdiff(t, string(golden), string(ndata))
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// An eqchecker holds state for checking the equality of two parse trees.
|
||||
type eqchecker struct {
|
||||
file string
|
||||
pos Position
|
||||
}
|
||||
|
||||
// errorf returns an error described by the printf-style format and arguments,
|
||||
// inserting the current file position before the error text.
|
||||
func (eq *eqchecker) errorf(format string, args ...interface{}) error {
|
||||
return fmt.Errorf("%s:%d: %s", eq.file, eq.pos.Line,
|
||||
fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// check checks that v and w represent the same parse tree.
|
||||
// If not, it returns an error describing the first difference.
|
||||
func (eq *eqchecker) check(v, w interface{}) error {
|
||||
return eq.checkValue(reflect.ValueOf(v), reflect.ValueOf(w))
|
||||
}
|
||||
|
||||
var (
|
||||
posType = reflect.TypeOf(Position{})
|
||||
commentsType = reflect.TypeOf(Comments{})
|
||||
)
|
||||
|
||||
// checkValue checks that v and w represent the same parse tree.
|
||||
// If not, it returns an error describing the first difference.
|
||||
func (eq *eqchecker) checkValue(v, w reflect.Value) error {
|
||||
// inner returns the innermost expression for v.
|
||||
// if v is a non-nil interface value, it returns the concrete
|
||||
// value in the interface.
|
||||
inner := func(v reflect.Value) reflect.Value {
|
||||
for {
|
||||
if v.Kind() == reflect.Interface && !v.IsNil() {
|
||||
v = v.Elem()
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
v = inner(v)
|
||||
w = inner(w)
|
||||
if v.Kind() == reflect.Invalid && w.Kind() == reflect.Invalid {
|
||||
return nil
|
||||
}
|
||||
if v.Kind() == reflect.Invalid {
|
||||
return eq.errorf("nil interface became %s", w.Type())
|
||||
}
|
||||
if w.Kind() == reflect.Invalid {
|
||||
return eq.errorf("%s became nil interface", v.Type())
|
||||
}
|
||||
|
||||
if v.Type() != w.Type() {
|
||||
return eq.errorf("%s became %s", v.Type(), w.Type())
|
||||
}
|
||||
|
||||
if p, ok := v.Interface().(Expr); ok {
|
||||
eq.pos, _ = p.Span()
|
||||
}
|
||||
|
||||
switch v.Kind() {
|
||||
default:
|
||||
return eq.errorf("unexpected type %s", v.Type())
|
||||
|
||||
case reflect.Bool, reflect.Int, reflect.String:
|
||||
vi := v.Interface()
|
||||
wi := w.Interface()
|
||||
if vi != wi {
|
||||
return eq.errorf("%v became %v", vi, wi)
|
||||
}
|
||||
|
||||
case reflect.Slice:
|
||||
vl := v.Len()
|
||||
wl := w.Len()
|
||||
for i := 0; i < vl || i < wl; i++ {
|
||||
if i >= vl {
|
||||
return eq.errorf("unexpected %s", w.Index(i).Type())
|
||||
}
|
||||
if i >= wl {
|
||||
return eq.errorf("missing %s", v.Index(i).Type())
|
||||
}
|
||||
if err := eq.checkValue(v.Index(i), w.Index(i)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
case reflect.Struct:
|
||||
// Fields in struct must match.
|
||||
t := v.Type()
|
||||
n := t.NumField()
|
||||
for i := 0; i < n; i++ {
|
||||
tf := t.Field(i)
|
||||
switch {
|
||||
default:
|
||||
if err := eq.checkValue(v.Field(i), w.Field(i)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
case tf.Type == posType: // ignore positions
|
||||
case tf.Type == commentsType: // ignore comment assignment
|
||||
}
|
||||
}
|
||||
|
||||
case reflect.Ptr, reflect.Interface:
|
||||
if v.IsNil() != w.IsNil() {
|
||||
if v.IsNil() {
|
||||
return eq.errorf("unexpected %s", w.Elem().Type())
|
||||
}
|
||||
return eq.errorf("missing %s", v.Elem().Type())
|
||||
}
|
||||
if err := eq.checkValue(v.Elem(), w.Elem()); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// diff returns the output of running diff on b1 and b2.
|
||||
func diff(b1, b2 []byte) (data []byte, err error) {
|
||||
f1, err := os.CreateTemp("", "testdiff")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.Remove(f1.Name())
|
||||
defer f1.Close()
|
||||
|
||||
f2, err := os.CreateTemp("", "testdiff")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer os.Remove(f2.Name())
|
||||
defer f2.Close()
|
||||
|
||||
f1.Write(b1)
|
||||
f2.Write(b2)
|
||||
|
||||
data, err = exec.Command("diff", "-u", f1.Name(), f2.Name()).CombinedOutput()
|
||||
if len(data) > 0 {
|
||||
// diff exits with a non-zero status when the files don't match.
|
||||
// Ignore that failure as long as we get output.
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// tdiff logs the diff output to t.Error.
|
||||
func tdiff(t *testing.T, a, b string) {
|
||||
data, err := diff([]byte(a), []byte(b))
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
t.Error(string(data))
|
||||
}
|
||||
|
||||
var modulePathTests = []struct {
|
||||
input []byte
|
||||
expected string
|
||||
}{
|
||||
{input: []byte("module \"github.com/rsc/vgotest\""), expected: "github.com/rsc/vgotest"},
|
||||
{input: []byte("module github.com/rsc/vgotest"), expected: "github.com/rsc/vgotest"},
|
||||
{input: []byte("module \"github.com/rsc/vgotest\""), expected: "github.com/rsc/vgotest"},
|
||||
{input: []byte("module github.com/rsc/vgotest"), expected: "github.com/rsc/vgotest"},
|
||||
{input: []byte("module `github.com/rsc/vgotest`"), expected: "github.com/rsc/vgotest"},
|
||||
{input: []byte("module \"github.com/rsc/vgotest/v2\""), expected: "github.com/rsc/vgotest/v2"},
|
||||
{input: []byte("module github.com/rsc/vgotest/v2"), expected: "github.com/rsc/vgotest/v2"},
|
||||
{input: []byte("module \"gopkg.in/yaml.v2\""), expected: "gopkg.in/yaml.v2"},
|
||||
{input: []byte("module gopkg.in/yaml.v2"), expected: "gopkg.in/yaml.v2"},
|
||||
{input: []byte("module \"gopkg.in/check.v1\"\n"), expected: "gopkg.in/check.v1"},
|
||||
{input: []byte("module \"gopkg.in/check.v1\n\""), expected: ""},
|
||||
{input: []byte("module gopkg.in/check.v1\n"), expected: "gopkg.in/check.v1"},
|
||||
{input: []byte("module \"gopkg.in/check.v1\"\r\n"), expected: "gopkg.in/check.v1"},
|
||||
{input: []byte("module gopkg.in/check.v1\r\n"), expected: "gopkg.in/check.v1"},
|
||||
{input: []byte("module \"gopkg.in/check.v1\"\n\n"), expected: "gopkg.in/check.v1"},
|
||||
{input: []byte("module gopkg.in/check.v1\n\n"), expected: "gopkg.in/check.v1"},
|
||||
{input: []byte("module \n\"gopkg.in/check.v1\"\n\n"), expected: ""},
|
||||
{input: []byte("module \ngopkg.in/check.v1\n\n"), expected: ""},
|
||||
{input: []byte("module \"gopkg.in/check.v1\"asd"), expected: ""},
|
||||
{input: []byte("module \n\"gopkg.in/check.v1\"\n\n"), expected: ""},
|
||||
{input: []byte("module \ngopkg.in/check.v1\n\n"), expected: ""},
|
||||
{input: []byte("module \"gopkg.in/check.v1\"asd"), expected: ""},
|
||||
{input: []byte("module \nmodule a/b/c "), expected: "a/b/c"},
|
||||
{input: []byte("module \" \""), expected: " "},
|
||||
{input: []byte("module "), expected: ""},
|
||||
{input: []byte("module \" a/b/c \""), expected: " a/b/c "},
|
||||
{input: []byte("module \"github.com/rsc/vgotest1\" // with a comment"), expected: "github.com/rsc/vgotest1"},
|
||||
}
|
||||
|
||||
func TestModulePath(t *testing.T) {
|
||||
for _, test := range modulePathTests {
|
||||
t.Run(string(test.input), func(t *testing.T) {
|
||||
result := ModulePath(test.input)
|
||||
if result != test.expected {
|
||||
t.Fatalf("ModulePath(%q): %s, want %s", string(test.input), result, test.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseVersions(t *testing.T) {
|
||||
tests := []struct {
|
||||
desc, input string
|
||||
ok bool
|
||||
laxOK bool // ok=true implies laxOK=true; only set if ok=false
|
||||
}{
|
||||
// go lines
|
||||
{desc: "empty", input: "module m\ngo \n", ok: false},
|
||||
{desc: "one", input: "module m\ngo 1\n", ok: false},
|
||||
{desc: "two", input: "module m\ngo 1.22\n", ok: true},
|
||||
{desc: "three", input: "module m\ngo 1.22.333", ok: true},
|
||||
{desc: "before", input: "module m\ngo v1.2\n", ok: false},
|
||||
{desc: "after", input: "module m\ngo 1.2rc1\n", ok: true},
|
||||
{desc: "space", input: "module m\ngo 1.2 3.4\n", ok: false},
|
||||
{desc: "alt1", input: "module m\ngo 1.2.3\n", ok: true},
|
||||
{desc: "alt2", input: "module m\ngo 1.2rc1\n", ok: true},
|
||||
{desc: "alt3", input: "module m\ngo 1.2beta1\n", ok: true},
|
||||
{desc: "alt4", input: "module m\ngo 1.2.beta1\n", ok: false, laxOK: true},
|
||||
{desc: "alt1", input: "module m\ngo v1.2.3\n", ok: false, laxOK: true},
|
||||
{desc: "alt2", input: "module m\ngo v1.2rc1\n", ok: false, laxOK: true},
|
||||
{desc: "alt3", input: "module m\ngo v1.2beta1\n", ok: false, laxOK: true},
|
||||
{desc: "alt4", input: "module m\ngo v1.2.beta1\n", ok: false, laxOK: true},
|
||||
{desc: "alt1", input: "module m\ngo v1.2\n", ok: false, laxOK: true},
|
||||
|
||||
// toolchain lines
|
||||
{desc: "tool", input: "module m\ntoolchain go1.2\n", ok: true},
|
||||
{desc: "tool1", input: "module m\ntoolchain go1.2.3\n", ok: true},
|
||||
{desc: "tool2", input: "module m\ntoolchain go1.2rc1\n", ok: true},
|
||||
{desc: "tool3", input: "module m\ntoolchain go1.2rc1-gccgo\n", ok: true},
|
||||
{desc: "tool4", input: "module m\ntoolchain default\n", ok: true},
|
||||
{desc: "tool5", input: "module m\ntoolchain inconceivable!\n", ok: false, laxOK: true},
|
||||
}
|
||||
t.Run("Strict", func(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
if _, err := Parse("go.mod", []byte(test.input), nil); err == nil && !test.ok {
|
||||
t.Error("unexpected success")
|
||||
} else if err != nil && test.ok {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
t.Run("Lax", func(t *testing.T) {
|
||||
for _, test := range tests {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
if _, err := ParseLax("go.mod", []byte(test.input), nil); err == nil && !(test.ok || test.laxOK) {
|
||||
t.Error("unexpected success")
|
||||
} else if err != nil && test.ok {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestComments(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
desc, input, want string
|
||||
}{
|
||||
{
|
||||
desc: "comment_only",
|
||||
input: `
|
||||
// a
|
||||
// b
|
||||
`,
|
||||
want: `
|
||||
comments before "// a"
|
||||
comments before "// b"
|
||||
`,
|
||||
}, {
|
||||
desc: "line",
|
||||
input: `
|
||||
// a
|
||||
|
||||
// b
|
||||
module m // c
|
||||
// d
|
||||
|
||||
// e
|
||||
`,
|
||||
want: `
|
||||
comments before "// a"
|
||||
line before "// b"
|
||||
line suffix "// c"
|
||||
comments before "// d"
|
||||
comments before "// e"
|
||||
`,
|
||||
}, {
|
||||
desc: "block",
|
||||
input: `
|
||||
// a
|
||||
|
||||
// b
|
||||
block ( // c
|
||||
// d
|
||||
|
||||
// e
|
||||
x // f
|
||||
// g
|
||||
|
||||
// h
|
||||
) // i
|
||||
// j
|
||||
|
||||
// k
|
||||
`,
|
||||
want: `
|
||||
comments before "// a"
|
||||
block before "// b"
|
||||
lparen suffix "// c"
|
||||
blockline before "// d"
|
||||
blockline before ""
|
||||
blockline before "// e"
|
||||
blockline suffix "// f"
|
||||
rparen before "// g"
|
||||
rparen before ""
|
||||
rparen before "// h"
|
||||
rparen suffix "// i"
|
||||
comments before "// j"
|
||||
comments before "// k"
|
||||
`,
|
||||
}, {
|
||||
desc: "cr_removed",
|
||||
input: "// a\r\r\n",
|
||||
want: `comments before "// a\r"`,
|
||||
},
|
||||
} {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
f, err := ParseLax("go.mod", []byte(test.input), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
printComments := func(prefix string, cs *Comments) {
|
||||
for _, c := range cs.Before {
|
||||
fmt.Fprintf(buf, "%s before %q\n", prefix, c.Token)
|
||||
}
|
||||
for _, c := range cs.Suffix {
|
||||
fmt.Fprintf(buf, "%s suffix %q\n", prefix, c.Token)
|
||||
}
|
||||
for _, c := range cs.After {
|
||||
fmt.Fprintf(buf, "%s after %q\n", prefix, c.Token)
|
||||
}
|
||||
}
|
||||
|
||||
printComments("file", &f.Syntax.Comments)
|
||||
for _, stmt := range f.Syntax.Stmt {
|
||||
switch stmt := stmt.(type) {
|
||||
case *CommentBlock:
|
||||
printComments("comments", stmt.Comment())
|
||||
case *Line:
|
||||
printComments("line", stmt.Comment())
|
||||
case *LineBlock:
|
||||
printComments("block", stmt.Comment())
|
||||
printComments("lparen", stmt.LParen.Comment())
|
||||
for _, line := range stmt.Line {
|
||||
printComments("blockline", line.Comment())
|
||||
}
|
||||
printComments("rparen", stmt.RParen.Comment())
|
||||
}
|
||||
}
|
||||
|
||||
got := strings.TrimSpace(buf.String())
|
||||
want := strings.TrimSpace(test.want)
|
||||
if got != want {
|
||||
t.Errorf("got:\n%s\nwant:\n%s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
||||
// comment
|
||||
x "y" z
|
||||
|
||||
// block
|
||||
block ( // block-eol
|
||||
// x-before-line
|
||||
|
||||
"x" (y // x-eol
|
||||
"x") y // y-eol
|
||||
"x1"
|
||||
"x2"
|
||||
// line
|
||||
"x3"
|
||||
"x4"
|
||||
|
||||
"x5"
|
||||
|
||||
// y-line
|
||||
"y" // y-eol
|
||||
|
||||
"z" // z-eol
|
||||
) // block-eol2
|
||||
|
||||
block1 (
|
||||
)
|
||||
|
||||
block2 (x y z)
|
||||
|
||||
block3 "w" (
|
||||
) // empty block
|
||||
|
||||
block4 "x" () "y" // not a block
|
||||
|
||||
block5 ("z" // also not a block
|
||||
|
||||
// eof
|
||||
@@ -0,0 +1,33 @@
|
||||
// comment
|
||||
x "y" z
|
||||
|
||||
// block
|
||||
block ( // block-eol
|
||||
// x-before-line
|
||||
|
||||
"x" ( y // x-eol
|
||||
"x" ) y // y-eol
|
||||
"x1"
|
||||
"x2"
|
||||
// line
|
||||
"x3"
|
||||
"x4"
|
||||
|
||||
"x5"
|
||||
|
||||
// y-line
|
||||
"y" // y-eol
|
||||
|
||||
"z" // z-eol
|
||||
) // block-eol2
|
||||
|
||||
|
||||
block1()
|
||||
|
||||
block2 (x y z)
|
||||
|
||||
block3 "w" ( ) // empty block
|
||||
block4 "x" ( ) "y" // not a block
|
||||
block5 ( "z" // also not a block
|
||||
|
||||
// eof
|
||||
@@ -0,0 +1,10 @@
|
||||
// comment
|
||||
module "x" // eol
|
||||
|
||||
// mid comment
|
||||
|
||||
// comment 2
|
||||
// comment 2 line 2
|
||||
module "y" // eoy
|
||||
|
||||
// comment 3
|
||||
@@ -0,0 +1,8 @@
|
||||
// comment
|
||||
module "x" // eol
|
||||
// mid comment
|
||||
|
||||
// comment 2
|
||||
// comment 2 line 2
|
||||
module "y" // eoy
|
||||
// comment 3
|
||||
@@ -0,0 +1,3 @@
|
||||
go 1.2.3
|
||||
|
||||
toolchain default
|
||||
@@ -0,0 +1,2 @@
|
||||
go 1.2.3
|
||||
toolchain default
|
||||
@@ -0,0 +1,6 @@
|
||||
module x
|
||||
|
||||
require (
|
||||
gopkg.in/mgo.v2 v2.0.0-20160818020120-3f83fa500528
|
||||
gopkg.in/yaml.v2 v2.2.1
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
module abc
|
||||
@@ -0,0 +1 @@
|
||||
module "abc"
|
||||
@@ -0,0 +1,12 @@
|
||||
module abc
|
||||
|
||||
replace xyz v1.2.3 => /tmp/z
|
||||
|
||||
replace xyz v1.3.4 => my/xyz v1.3.4-me
|
||||
|
||||
replace (
|
||||
w v1.0.0 => "./a,"
|
||||
w v1.0.1 => "./a()"
|
||||
w v1.0.2 => "./a[]"
|
||||
w v1.0.3 => "./a{}"
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
module "abc"
|
||||
|
||||
replace "xyz" v1.2.3 => "/tmp/z"
|
||||
|
||||
replace "xyz" v1.3.4 => "my/xyz" v1.3.4-me
|
||||
|
||||
replace (
|
||||
"w" v1.0.0 => "./a,"
|
||||
"w" v1.0.1 => "./a()"
|
||||
"w" v1.0.2 => "./a[]"
|
||||
"w" v1.0.3 => "./a{}"
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
module abc
|
||||
|
||||
replace (
|
||||
xyz v1.2.3 => /tmp/z
|
||||
xyz v1.3.4 => my/xyz v1.3.4-me
|
||||
xyz v1.4.5 => "/tmp/my dir"
|
||||
xyz v1.5.6 => my/xyz v1.5.6
|
||||
|
||||
xyz => my/other/xyz v1.5.4
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
module "abc"
|
||||
|
||||
replace (
|
||||
"xyz" v1.2.3 => "/tmp/z"
|
||||
"xyz" v1.3.4 => "my/xyz" "v1.3.4-me"
|
||||
xyz "v1.4.5" => "/tmp/my dir"
|
||||
xyz v1.5.6 => my/xyz v1.5.6
|
||||
|
||||
xyz => my/other/xyz v1.5.4
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
module abc
|
||||
|
||||
retract v1.2.3
|
||||
|
||||
retract [v1.2.3, v1.2.4]
|
||||
|
||||
retract (
|
||||
v1.2.3
|
||||
|
||||
[v1.2.3, v1.2.4]
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
module abc
|
||||
|
||||
retract "v1.2.3"
|
||||
|
||||
retract [ "v1.2.3" , "v1.2.4" ]
|
||||
|
||||
retract (
|
||||
"v1.2.3"
|
||||
|
||||
[ "v1.2.3" , "v1.2.4" ]
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
module "x"
|
||||
|
||||
module "y"
|
||||
|
||||
require "x"
|
||||
|
||||
require x
|
||||
@@ -0,0 +1,10 @@
|
||||
// comment
|
||||
use x // eol
|
||||
|
||||
// mid comment
|
||||
|
||||
// comment 2
|
||||
// comment 2 line 2
|
||||
use y // eoy
|
||||
|
||||
// comment 3
|
||||
@@ -0,0 +1,8 @@
|
||||
// comment
|
||||
use "x" // eol
|
||||
// mid comment
|
||||
|
||||
// comment 2
|
||||
// comment 2 line 2
|
||||
use "y" // eoy
|
||||
// comment 3
|
||||
@@ -0,0 +1,3 @@
|
||||
go 1.2.3
|
||||
|
||||
toolchain default
|
||||
@@ -0,0 +1,2 @@
|
||||
go 1.2.3
|
||||
toolchain default
|
||||
@@ -0,0 +1,12 @@
|
||||
use abc
|
||||
|
||||
replace xyz v1.2.3 => /tmp/z
|
||||
|
||||
replace xyz v1.3.4 => my/xyz v1.3.4-me
|
||||
|
||||
replace (
|
||||
w v1.0.0 => "./a,"
|
||||
w v1.0.1 => "./a()"
|
||||
w v1.0.2 => "./a[]"
|
||||
w v1.0.3 => "./a{}"
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
use "abc"
|
||||
|
||||
replace "xyz" v1.2.3 => "/tmp/z"
|
||||
|
||||
replace "xyz" v1.3.4 => "my/xyz" v1.3.4-me
|
||||
|
||||
replace (
|
||||
"w" v1.0.0 => "./a,"
|
||||
"w" v1.0.1 => "./a()"
|
||||
"w" v1.0.2 => "./a[]"
|
||||
"w" v1.0.3 => "./a{}"
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
use abc
|
||||
|
||||
replace (
|
||||
xyz v1.2.3 => /tmp/z
|
||||
xyz v1.3.4 => my/xyz v1.3.4-me
|
||||
xyz v1.4.5 => "/tmp/my dir"
|
||||
xyz v1.5.6 => my/xyz v1.5.6
|
||||
|
||||
xyz => my/other/xyz v1.5.4
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
use "abc"
|
||||
|
||||
replace (
|
||||
"xyz" v1.2.3 => "/tmp/z"
|
||||
"xyz" v1.3.4 => "my/xyz" "v1.3.4-me"
|
||||
xyz "v1.4.5" => "/tmp/my dir"
|
||||
xyz v1.5.6 => my/xyz v1.5.6
|
||||
|
||||
xyz => my/other/xyz v1.5.4
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
use ../foo
|
||||
|
||||
use (
|
||||
/bar
|
||||
|
||||
baz
|
||||
)
|
||||
@@ -0,0 +1,7 @@
|
||||
use "../foo"
|
||||
|
||||
use (
|
||||
"/bar"
|
||||
|
||||
"baz"
|
||||
)
|
||||
@@ -0,0 +1,285 @@
|
||||
// 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 modfile
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A WorkFile is the parsed, interpreted form of a go.work file.
|
||||
type WorkFile struct {
|
||||
Go *Go
|
||||
Toolchain *Toolchain
|
||||
Use []*Use
|
||||
Replace []*Replace
|
||||
|
||||
Syntax *FileSyntax
|
||||
}
|
||||
|
||||
// A Use is a single directory statement.
|
||||
type Use struct {
|
||||
Path string // Use path of module.
|
||||
ModulePath string // Module path in the comment.
|
||||
Syntax *Line
|
||||
}
|
||||
|
||||
// ParseWork parses and returns a go.work file.
|
||||
//
|
||||
// file is the name of the file, used in positions and errors.
|
||||
//
|
||||
// data is the content of the file.
|
||||
//
|
||||
// fix is an optional function that canonicalizes module versions.
|
||||
// If fix is nil, all module versions must be canonical ([module.CanonicalVersion]
|
||||
// must return the same string).
|
||||
func ParseWork(file string, data []byte, fix VersionFixer) (*WorkFile, error) {
|
||||
fs, err := parse(file, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f := &WorkFile{
|
||||
Syntax: fs,
|
||||
}
|
||||
var errs ErrorList
|
||||
|
||||
for _, x := range fs.Stmt {
|
||||
switch x := x.(type) {
|
||||
case *Line:
|
||||
f.add(&errs, x, x.Token[0], x.Token[1:], fix)
|
||||
|
||||
case *LineBlock:
|
||||
if len(x.Token) > 1 {
|
||||
errs = append(errs, Error{
|
||||
Filename: file,
|
||||
Pos: x.Start,
|
||||
Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
|
||||
})
|
||||
continue
|
||||
}
|
||||
switch x.Token[0] {
|
||||
default:
|
||||
errs = append(errs, Error{
|
||||
Filename: file,
|
||||
Pos: x.Start,
|
||||
Err: fmt.Errorf("unknown block type: %s", strings.Join(x.Token, " ")),
|
||||
})
|
||||
continue
|
||||
case "use", "replace":
|
||||
for _, l := range x.Line {
|
||||
f.add(&errs, l, x.Token[0], l.Token, fix)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return nil, errs
|
||||
}
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Cleanup cleans up the file f after any edit operations.
|
||||
// To avoid quadratic behavior, modifications like [WorkFile.DropRequire]
|
||||
// clear the entry but do not remove it from the slice.
|
||||
// Cleanup cleans out all the cleared entries.
|
||||
func (f *WorkFile) Cleanup() {
|
||||
w := 0
|
||||
for _, r := range f.Use {
|
||||
if r.Path != "" {
|
||||
f.Use[w] = r
|
||||
w++
|
||||
}
|
||||
}
|
||||
f.Use = f.Use[:w]
|
||||
|
||||
w = 0
|
||||
for _, r := range f.Replace {
|
||||
if r.Old.Path != "" {
|
||||
f.Replace[w] = r
|
||||
w++
|
||||
}
|
||||
}
|
||||
f.Replace = f.Replace[:w]
|
||||
|
||||
f.Syntax.Cleanup()
|
||||
}
|
||||
|
||||
func (f *WorkFile) AddGoStmt(version string) error {
|
||||
if !GoVersionRE.MatchString(version) {
|
||||
return fmt.Errorf("invalid language version %q", version)
|
||||
}
|
||||
if f.Go == nil {
|
||||
stmt := &Line{Token: []string{"go", version}}
|
||||
f.Go = &Go{
|
||||
Version: version,
|
||||
Syntax: stmt,
|
||||
}
|
||||
// Find the first non-comment-only block and add
|
||||
// the go statement before it. That will keep file comments at the top.
|
||||
i := 0
|
||||
for i = 0; i < len(f.Syntax.Stmt); i++ {
|
||||
if _, ok := f.Syntax.Stmt[i].(*CommentBlock); !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
f.Syntax.Stmt = append(append(f.Syntax.Stmt[:i:i], stmt), f.Syntax.Stmt[i:]...)
|
||||
} else {
|
||||
f.Go.Version = version
|
||||
f.Syntax.updateLine(f.Go.Syntax, "go", version)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *WorkFile) AddToolchainStmt(name string) error {
|
||||
if !ToolchainRE.MatchString(name) {
|
||||
return fmt.Errorf("invalid toolchain name %q", name)
|
||||
}
|
||||
if f.Toolchain == nil {
|
||||
stmt := &Line{Token: []string{"toolchain", name}}
|
||||
f.Toolchain = &Toolchain{
|
||||
Name: name,
|
||||
Syntax: stmt,
|
||||
}
|
||||
// Find the go line and add the toolchain line after it.
|
||||
// Or else find the first non-comment-only block and add
|
||||
// the toolchain line before it. That will keep file comments at the top.
|
||||
i := 0
|
||||
for i = 0; i < len(f.Syntax.Stmt); i++ {
|
||||
if line, ok := f.Syntax.Stmt[i].(*Line); ok && len(line.Token) > 0 && line.Token[0] == "go" {
|
||||
i++
|
||||
goto Found
|
||||
}
|
||||
}
|
||||
for i = 0; i < len(f.Syntax.Stmt); i++ {
|
||||
if _, ok := f.Syntax.Stmt[i].(*CommentBlock); !ok {
|
||||
break
|
||||
}
|
||||
}
|
||||
Found:
|
||||
f.Syntax.Stmt = append(append(f.Syntax.Stmt[:i:i], stmt), f.Syntax.Stmt[i:]...)
|
||||
} else {
|
||||
f.Toolchain.Name = name
|
||||
f.Syntax.updateLine(f.Toolchain.Syntax, "toolchain", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// DropGoStmt deletes the go statement from the file.
|
||||
func (f *WorkFile) DropGoStmt() {
|
||||
if f.Go != nil {
|
||||
f.Go.Syntax.markRemoved()
|
||||
f.Go = nil
|
||||
}
|
||||
}
|
||||
|
||||
// DropToolchainStmt deletes the toolchain statement from the file.
|
||||
func (f *WorkFile) DropToolchainStmt() {
|
||||
if f.Toolchain != nil {
|
||||
f.Toolchain.Syntax.markRemoved()
|
||||
f.Toolchain = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *WorkFile) AddUse(diskPath, modulePath string) error {
|
||||
need := true
|
||||
for _, d := range f.Use {
|
||||
if d.Path == diskPath {
|
||||
if need {
|
||||
d.ModulePath = modulePath
|
||||
f.Syntax.updateLine(d.Syntax, "use", AutoQuote(diskPath))
|
||||
need = false
|
||||
} else {
|
||||
d.Syntax.markRemoved()
|
||||
*d = Use{}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if need {
|
||||
f.AddNewUse(diskPath, modulePath)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *WorkFile) AddNewUse(diskPath, modulePath string) {
|
||||
line := f.Syntax.addLine(nil, "use", AutoQuote(diskPath))
|
||||
f.Use = append(f.Use, &Use{Path: diskPath, ModulePath: modulePath, Syntax: line})
|
||||
}
|
||||
|
||||
func (f *WorkFile) SetUse(dirs []*Use) {
|
||||
need := make(map[string]string)
|
||||
for _, d := range dirs {
|
||||
need[d.Path] = d.ModulePath
|
||||
}
|
||||
|
||||
for _, d := range f.Use {
|
||||
if modulePath, ok := need[d.Path]; ok {
|
||||
d.ModulePath = modulePath
|
||||
} else {
|
||||
d.Syntax.markRemoved()
|
||||
*d = Use{}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(#45713): Add module path to comment.
|
||||
|
||||
for diskPath, modulePath := range need {
|
||||
f.AddNewUse(diskPath, modulePath)
|
||||
}
|
||||
f.SortBlocks()
|
||||
}
|
||||
|
||||
func (f *WorkFile) DropUse(path string) error {
|
||||
for _, d := range f.Use {
|
||||
if d.Path == path {
|
||||
d.Syntax.markRemoved()
|
||||
*d = Use{}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *WorkFile) AddReplace(oldPath, oldVers, newPath, newVers string) error {
|
||||
return addReplace(f.Syntax, &f.Replace, oldPath, oldVers, newPath, newVers)
|
||||
}
|
||||
|
||||
func (f *WorkFile) DropReplace(oldPath, oldVers string) error {
|
||||
for _, r := range f.Replace {
|
||||
if r.Old.Path == oldPath && r.Old.Version == oldVers {
|
||||
r.Syntax.markRemoved()
|
||||
*r = Replace{}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *WorkFile) SortBlocks() {
|
||||
f.removeDups() // otherwise sorting is unsafe
|
||||
|
||||
for _, stmt := range f.Syntax.Stmt {
|
||||
block, ok := stmt.(*LineBlock)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
sort.SliceStable(block.Line, func(i, j int) bool {
|
||||
return lineLess(block.Line[i], block.Line[j])
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// removeDups removes duplicate replace directives.
|
||||
//
|
||||
// Later replace directives take priority.
|
||||
//
|
||||
// require directives are not de-duplicated. That's left up to higher-level
|
||||
// logic (MVS).
|
||||
//
|
||||
// retract directives are not de-duplicated since comments are
|
||||
// meaningful, and versions may be retracted multiple times.
|
||||
func (f *WorkFile) removeDups() {
|
||||
removeDups(f.Syntax, nil, &f.Replace)
|
||||
}
|
||||
@@ -0,0 +1,456 @@
|
||||
// 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 modfile
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TODO(#45713): Update these tests once AddUse sets the module path.
|
||||
var workAddUseTests = []struct {
|
||||
desc string
|
||||
in string
|
||||
path string
|
||||
modulePath string
|
||||
out string
|
||||
}{
|
||||
{
|
||||
`empty`,
|
||||
``,
|
||||
`foo`, `bar`,
|
||||
`use foo`,
|
||||
},
|
||||
{
|
||||
`go_stmt_only`,
|
||||
`go 1.17
|
||||
`,
|
||||
`foo`, `bar`,
|
||||
`go 1.17
|
||||
use foo
|
||||
`,
|
||||
},
|
||||
{
|
||||
`use_line_present`,
|
||||
`go 1.17
|
||||
use baz`,
|
||||
`foo`, `bar`,
|
||||
`go 1.17
|
||||
use (
|
||||
baz
|
||||
foo
|
||||
)
|
||||
`,
|
||||
},
|
||||
{
|
||||
`use_block_present`,
|
||||
`go 1.17
|
||||
use (
|
||||
baz
|
||||
quux
|
||||
)
|
||||
`,
|
||||
`foo`, `bar`,
|
||||
`go 1.17
|
||||
use (
|
||||
baz
|
||||
quux
|
||||
foo
|
||||
)
|
||||
`,
|
||||
},
|
||||
{
|
||||
`use_and_replace_present`,
|
||||
`go 1.17
|
||||
use baz
|
||||
replace a => ./b
|
||||
`,
|
||||
`foo`, `bar`,
|
||||
`go 1.17
|
||||
use (
|
||||
baz
|
||||
foo
|
||||
)
|
||||
replace a => ./b
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
var workDropUseTests = []struct {
|
||||
desc string
|
||||
in string
|
||||
path string
|
||||
out string
|
||||
}{
|
||||
{
|
||||
`empty`,
|
||||
``,
|
||||
`foo`,
|
||||
``,
|
||||
},
|
||||
{
|
||||
`go_stmt_only`,
|
||||
`go 1.17
|
||||
`,
|
||||
`foo`,
|
||||
`go 1.17
|
||||
`,
|
||||
},
|
||||
{
|
||||
`single_use`,
|
||||
`go 1.17
|
||||
use foo`,
|
||||
`foo`,
|
||||
`go 1.17
|
||||
`,
|
||||
},
|
||||
{
|
||||
`use_block`,
|
||||
`go 1.17
|
||||
use (
|
||||
foo
|
||||
bar
|
||||
baz
|
||||
)`,
|
||||
`bar`,
|
||||
`go 1.17
|
||||
use (
|
||||
foo
|
||||
baz
|
||||
)`,
|
||||
},
|
||||
{
|
||||
`use_multi`,
|
||||
`go 1.17
|
||||
use (
|
||||
foo
|
||||
bar
|
||||
baz
|
||||
)
|
||||
use foo
|
||||
use quux
|
||||
use foo`,
|
||||
`foo`,
|
||||
`go 1.17
|
||||
use (
|
||||
bar
|
||||
baz
|
||||
)
|
||||
use quux`,
|
||||
},
|
||||
}
|
||||
|
||||
var workAddGoTests = []struct {
|
||||
desc string
|
||||
in string
|
||||
version string
|
||||
out string
|
||||
}{
|
||||
{
|
||||
`empty`,
|
||||
``,
|
||||
`1.17`,
|
||||
`go 1.17
|
||||
`,
|
||||
},
|
||||
{
|
||||
`comment`,
|
||||
`// this is a comment`,
|
||||
`1.17`,
|
||||
`// this is a comment
|
||||
|
||||
go 1.17`,
|
||||
},
|
||||
{
|
||||
`use_after_replace`,
|
||||
`
|
||||
replace example.com/foo => ../bar
|
||||
use foo
|
||||
`,
|
||||
`1.17`,
|
||||
`
|
||||
go 1.17
|
||||
replace example.com/foo => ../bar
|
||||
use foo
|
||||
`,
|
||||
},
|
||||
{
|
||||
`use_before_replace`,
|
||||
`use foo
|
||||
replace example.com/foo => ../bar
|
||||
`,
|
||||
`1.17`,
|
||||
`
|
||||
go 1.17
|
||||
use foo
|
||||
replace example.com/foo => ../bar
|
||||
`,
|
||||
},
|
||||
{
|
||||
`use_only`,
|
||||
`use foo
|
||||
`,
|
||||
`1.17`,
|
||||
`
|
||||
go 1.17
|
||||
use foo
|
||||
`,
|
||||
},
|
||||
{
|
||||
`already_have_go`,
|
||||
`go 1.17
|
||||
`,
|
||||
`1.18`,
|
||||
`
|
||||
go 1.18
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
var workAddToolchainTests = []struct {
|
||||
desc string
|
||||
in string
|
||||
version string
|
||||
out string
|
||||
}{
|
||||
{
|
||||
`empty`,
|
||||
``,
|
||||
`go1.17`,
|
||||
`toolchain go1.17
|
||||
`,
|
||||
},
|
||||
{
|
||||
`aftergo`,
|
||||
`// this is a comment
|
||||
use foo
|
||||
|
||||
go 1.17
|
||||
|
||||
use bar
|
||||
`,
|
||||
`go1.17`,
|
||||
`// this is a comment
|
||||
use foo
|
||||
|
||||
go 1.17
|
||||
|
||||
toolchain go1.17
|
||||
|
||||
use bar
|
||||
`,
|
||||
},
|
||||
{
|
||||
`already_have_toolchain`,
|
||||
`go 1.17
|
||||
|
||||
toolchain go1.18
|
||||
`,
|
||||
`go1.19`,
|
||||
`go 1.17
|
||||
|
||||
toolchain go1.19
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
var workSortBlocksTests = []struct {
|
||||
desc, in, out string
|
||||
}{
|
||||
{
|
||||
`use_duplicates_not_removed`,
|
||||
`go 1.17
|
||||
use foo
|
||||
use bar
|
||||
use (
|
||||
foo
|
||||
)`,
|
||||
`go 1.17
|
||||
use foo
|
||||
use bar
|
||||
use (
|
||||
foo
|
||||
)`,
|
||||
},
|
||||
{
|
||||
`replace_duplicates_removed`,
|
||||
`go 1.17
|
||||
use foo
|
||||
replace x.y/z v1.0.0 => ./a
|
||||
replace x.y/z v1.1.0 => ./b
|
||||
replace (
|
||||
x.y/z v1.0.0 => ./c
|
||||
)
|
||||
`,
|
||||
`go 1.17
|
||||
use foo
|
||||
replace x.y/z v1.1.0 => ./b
|
||||
replace (
|
||||
x.y/z v1.0.0 => ./c
|
||||
)
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
||||
func TestAddUse(t *testing.T) {
|
||||
for _, tt := range workAddUseTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
testWorkEdit(t, tt.in, tt.out, func(f *WorkFile) error {
|
||||
return f.AddUse(tt.path, tt.modulePath)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDropUse(t *testing.T) {
|
||||
for _, tt := range workDropUseTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
testWorkEdit(t, tt.in, tt.out, func(f *WorkFile) error {
|
||||
if err := f.DropUse(tt.path); err != nil {
|
||||
return err
|
||||
}
|
||||
f.Cleanup()
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkAddGo(t *testing.T) {
|
||||
for _, tt := range workAddGoTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
testWorkEdit(t, tt.in, tt.out, func(f *WorkFile) error {
|
||||
return f.AddGoStmt(tt.version)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkAddToolchain(t *testing.T) {
|
||||
for _, tt := range workAddToolchainTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
testWorkEdit(t, tt.in, tt.out, func(f *WorkFile) error {
|
||||
return f.AddToolchainStmt(tt.version)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkSortBlocks(t *testing.T) {
|
||||
for _, tt := range workSortBlocksTests {
|
||||
t.Run(tt.desc, func(t *testing.T) {
|
||||
testWorkEdit(t, tt.in, tt.out, func(f *WorkFile) error {
|
||||
f.SortBlocks()
|
||||
return nil
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Test that when files in the testdata directory are parsed
|
||||
// and printed and parsed again, we get the same parse tree
|
||||
// both times.
|
||||
func TestWorkPrintParse(t *testing.T) {
|
||||
outs, err := filepath.Glob("testdata/work/*")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, out := range outs {
|
||||
out := out
|
||||
name := filepath.Base(out)
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
data, err := os.ReadFile(out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
base := "testdata/work/" + filepath.Base(out)
|
||||
f, err := parse(base, data)
|
||||
if err != nil {
|
||||
t.Fatalf("parsing original: %v", err)
|
||||
}
|
||||
|
||||
ndata := Format(f)
|
||||
f2, err := parse(base, ndata)
|
||||
if err != nil {
|
||||
t.Fatalf("parsing reformatted: %v", err)
|
||||
}
|
||||
|
||||
eq := eqchecker{file: base}
|
||||
if err := eq.check(f, f2); err != nil {
|
||||
t.Errorf("not equal (parse/Format/parse): %v", err)
|
||||
}
|
||||
|
||||
pf1, err := ParseWork(base, data, nil)
|
||||
if err != nil {
|
||||
t.Errorf("should parse %v: %v", base, err)
|
||||
}
|
||||
if err == nil {
|
||||
pf2, err := ParseWork(base, ndata, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Parsing reformatted: %v", err)
|
||||
}
|
||||
eq := eqchecker{file: base}
|
||||
if err := eq.check(pf1, pf2); err != nil {
|
||||
t.Errorf("not equal (parse/Format/Parse): %v", err)
|
||||
}
|
||||
|
||||
ndata2 := Format(pf1.Syntax)
|
||||
pf3, err := ParseWork(base, ndata2, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Parsing reformatted2: %v", err)
|
||||
}
|
||||
eq = eqchecker{file: base}
|
||||
if err := eq.check(pf1, pf3); err != nil {
|
||||
t.Errorf("not equal (Parse/Format/Parse): %v", err)
|
||||
}
|
||||
ndata = ndata2
|
||||
}
|
||||
|
||||
if strings.HasSuffix(out, ".in") {
|
||||
golden, err := os.ReadFile(strings.TrimSuffix(out, ".in") + ".golden")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(ndata, golden) {
|
||||
t.Errorf("formatted %s incorrectly: diff shows -golden, +ours", base)
|
||||
tdiff(t, string(golden), string(ndata))
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func testWorkEdit(t *testing.T, in, want string, transform func(f *WorkFile) error) *WorkFile {
|
||||
t.Helper()
|
||||
parse := ParseWork
|
||||
f, err := parse("in", []byte(in), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
g, err := parse("out", []byte(want), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
golden := Format(g.Syntax)
|
||||
|
||||
if err := transform(f); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := Format(f.Syntax)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(out, golden) {
|
||||
t.Errorf("have:\n%s\nwant:\n%s", out, golden)
|
||||
}
|
||||
|
||||
return f
|
||||
}
|
||||
@@ -0,0 +1,841 @@
|
||||
// Copyright 2018 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 module defines the module.Version type along with support code.
|
||||
//
|
||||
// The [module.Version] type is a simple Path, Version pair:
|
||||
//
|
||||
// type Version struct {
|
||||
// Path string
|
||||
// Version string
|
||||
// }
|
||||
//
|
||||
// There are no restrictions imposed directly by use of this structure,
|
||||
// but additional checking functions, most notably [Check], verify that
|
||||
// a particular path, version pair is valid.
|
||||
//
|
||||
// # Escaped Paths
|
||||
//
|
||||
// Module paths appear as substrings of file system paths
|
||||
// (in the download cache) and of web server URLs in the proxy protocol.
|
||||
// In general we cannot rely on file systems to be case-sensitive,
|
||||
// nor can we rely on web servers, since they read from file systems.
|
||||
// That is, we cannot rely on the file system to keep rsc.io/QUOTE
|
||||
// and rsc.io/quote separate. Windows and macOS don't.
|
||||
// Instead, we must never require two different casings of a file path.
|
||||
// Because we want the download cache to match the proxy protocol,
|
||||
// and because we want the proxy protocol to be possible to serve
|
||||
// from a tree of static files (which might be stored on a case-insensitive
|
||||
// file system), the proxy protocol must never require two different casings
|
||||
// of a URL path either.
|
||||
//
|
||||
// One possibility would be to make the escaped form be the lowercase
|
||||
// hexadecimal encoding of the actual path bytes. This would avoid ever
|
||||
// needing different casings of a file path, but it would be fairly illegible
|
||||
// to most programmers when those paths appeared in the file system
|
||||
// (including in file paths in compiler errors and stack traces)
|
||||
// in web server logs, and so on. Instead, we want a safe escaped form that
|
||||
// leaves most paths unaltered.
|
||||
//
|
||||
// The safe escaped form is to replace every uppercase letter
|
||||
// with an exclamation mark followed by the letter's lowercase equivalent.
|
||||
//
|
||||
// For example,
|
||||
//
|
||||
// github.com/Azure/azure-sdk-for-go -> github.com/!azure/azure-sdk-for-go.
|
||||
// github.com/GoogleCloudPlatform/cloudsql-proxy -> github.com/!google!cloud!platform/cloudsql-proxy
|
||||
// github.com/Sirupsen/logrus -> github.com/!sirupsen/logrus.
|
||||
//
|
||||
// Import paths that avoid upper-case letters are left unchanged.
|
||||
// Note that because import paths are ASCII-only and avoid various
|
||||
// problematic punctuation (like : < and >), the escaped form is also ASCII-only
|
||||
// and avoids the same problematic punctuation.
|
||||
//
|
||||
// Import paths have never allowed exclamation marks, so there is no
|
||||
// need to define how to escape a literal !.
|
||||
//
|
||||
// # Unicode Restrictions
|
||||
//
|
||||
// Today, paths are disallowed from using Unicode.
|
||||
//
|
||||
// Although paths are currently disallowed from using Unicode,
|
||||
// we would like at some point to allow Unicode letters as well, to assume that
|
||||
// file systems and URLs are Unicode-safe (storing UTF-8), and apply
|
||||
// the !-for-uppercase convention for escaping them in the file system.
|
||||
// But there are at least two subtle considerations.
|
||||
//
|
||||
// First, note that not all case-fold equivalent distinct runes
|
||||
// form an upper/lower pair.
|
||||
// For example, U+004B ('K'), U+006B ('k'), and U+212A ('K' for Kelvin)
|
||||
// are three distinct runes that case-fold to each other.
|
||||
// When we do add Unicode letters, we must not assume that upper/lower
|
||||
// are the only case-equivalent pairs.
|
||||
// Perhaps the Kelvin symbol would be disallowed entirely, for example.
|
||||
// Or perhaps it would escape as "!!k", or perhaps as "(212A)".
|
||||
//
|
||||
// Second, it would be nice to allow Unicode marks as well as letters,
|
||||
// but marks include combining marks, and then we must deal not
|
||||
// only with case folding but also normalization: both U+00E9 ('é')
|
||||
// and U+0065 U+0301 ('e' followed by combining acute accent)
|
||||
// look the same on the page and are treated by some file systems
|
||||
// as the same path. If we do allow Unicode marks in paths, there
|
||||
// must be some kind of normalization to allow only one canonical
|
||||
// encoding of any character used in an import path.
|
||||
package module
|
||||
|
||||
// IMPORTANT NOTE
|
||||
//
|
||||
// This file essentially defines the set of valid import paths for the go command.
|
||||
// There are many subtle considerations, including Unicode ambiguity,
|
||||
// security, network, and file system representations.
|
||||
//
|
||||
// This file also defines the set of valid module path and version combinations,
|
||||
// another topic with many subtle considerations.
|
||||
//
|
||||
// Changes to the semantics in this file require approval from rsc.
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
// A Version (for clients, a module.Version) is defined by a module path and version pair.
|
||||
// These are stored in their plain (unescaped) form.
|
||||
type Version struct {
|
||||
// Path is a module path, like "golang.org/x/text" or "rsc.io/quote/v2".
|
||||
Path string
|
||||
|
||||
// Version is usually a semantic version in canonical form.
|
||||
// There are three exceptions to this general rule.
|
||||
// First, the top-level target of a build has no specific version
|
||||
// and uses Version = "".
|
||||
// Second, during MVS calculations the version "none" is used
|
||||
// to represent the decision to take no version of a given module.
|
||||
// Third, filesystem paths found in "replace" directives are
|
||||
// represented by a path with an empty version.
|
||||
Version string `json:",omitempty"`
|
||||
}
|
||||
|
||||
// String returns a representation of the Version suitable for logging
|
||||
// (Path@Version, or just Path if Version is empty).
|
||||
func (m Version) String() string {
|
||||
if m.Version == "" {
|
||||
return m.Path
|
||||
}
|
||||
return m.Path + "@" + m.Version
|
||||
}
|
||||
|
||||
// A ModuleError indicates an error specific to a module.
|
||||
type ModuleError struct {
|
||||
Path string
|
||||
Version string
|
||||
Err error
|
||||
}
|
||||
|
||||
// VersionError returns a [ModuleError] derived from a [Version] and error,
|
||||
// or err itself if it is already such an error.
|
||||
func VersionError(v Version, err error) error {
|
||||
var mErr *ModuleError
|
||||
if errors.As(err, &mErr) && mErr.Path == v.Path && mErr.Version == v.Version {
|
||||
return err
|
||||
}
|
||||
return &ModuleError{
|
||||
Path: v.Path,
|
||||
Version: v.Version,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ModuleError) Error() string {
|
||||
if v, ok := e.Err.(*InvalidVersionError); ok {
|
||||
return fmt.Sprintf("%s@%s: invalid %s: %v", e.Path, v.Version, v.noun(), v.Err)
|
||||
}
|
||||
if e.Version != "" {
|
||||
return fmt.Sprintf("%s@%s: %v", e.Path, e.Version, e.Err)
|
||||
}
|
||||
return fmt.Sprintf("module %s: %v", e.Path, e.Err)
|
||||
}
|
||||
|
||||
func (e *ModuleError) Unwrap() error { return e.Err }
|
||||
|
||||
// An InvalidVersionError indicates an error specific to a version, with the
|
||||
// module path unknown or specified externally.
|
||||
//
|
||||
// A [ModuleError] may wrap an InvalidVersionError, but an InvalidVersionError
|
||||
// must not wrap a ModuleError.
|
||||
type InvalidVersionError struct {
|
||||
Version string
|
||||
Pseudo bool
|
||||
Err error
|
||||
}
|
||||
|
||||
// noun returns either "version" or "pseudo-version", depending on whether
|
||||
// e.Version is a pseudo-version.
|
||||
func (e *InvalidVersionError) noun() string {
|
||||
if e.Pseudo {
|
||||
return "pseudo-version"
|
||||
}
|
||||
return "version"
|
||||
}
|
||||
|
||||
func (e *InvalidVersionError) Error() string {
|
||||
return fmt.Sprintf("%s %q invalid: %s", e.noun(), e.Version, e.Err)
|
||||
}
|
||||
|
||||
func (e *InvalidVersionError) Unwrap() error { return e.Err }
|
||||
|
||||
// An InvalidPathError indicates a module, import, or file path doesn't
|
||||
// satisfy all naming constraints. See [CheckPath], [CheckImportPath],
|
||||
// and [CheckFilePath] for specific restrictions.
|
||||
type InvalidPathError struct {
|
||||
Kind string // "module", "import", or "file"
|
||||
Path string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *InvalidPathError) Error() string {
|
||||
return fmt.Sprintf("malformed %s path %q: %v", e.Kind, e.Path, e.Err)
|
||||
}
|
||||
|
||||
func (e *InvalidPathError) Unwrap() error { return e.Err }
|
||||
|
||||
// Check checks that a given module path, version pair is valid.
|
||||
// In addition to the path being a valid module path
|
||||
// and the version being a valid semantic version,
|
||||
// the two must correspond.
|
||||
// For example, the path "yaml/v2" only corresponds to
|
||||
// semantic versions beginning with "v2.".
|
||||
func Check(path, version string) error {
|
||||
if err := CheckPath(path); err != nil {
|
||||
return err
|
||||
}
|
||||
if !semver.IsValid(version) {
|
||||
return &ModuleError{
|
||||
Path: path,
|
||||
Err: &InvalidVersionError{Version: version, Err: errors.New("not a semantic version")},
|
||||
}
|
||||
}
|
||||
_, pathMajor, _ := SplitPathVersion(path)
|
||||
if err := CheckPathMajor(version, pathMajor); err != nil {
|
||||
return &ModuleError{Path: path, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// firstPathOK reports whether r can appear in the first element of a module path.
|
||||
// The first element of the path must be an LDH domain name, at least for now.
|
||||
// To avoid case ambiguity, the domain name must be entirely lower case.
|
||||
func firstPathOK(r rune) bool {
|
||||
return r == '-' || r == '.' ||
|
||||
'0' <= r && r <= '9' ||
|
||||
'a' <= r && r <= 'z'
|
||||
}
|
||||
|
||||
// modPathOK reports whether r can appear in a module path element.
|
||||
// Paths can be ASCII letters, ASCII digits, and limited ASCII punctuation: - . _ and ~.
|
||||
//
|
||||
// This matches what "go get" has historically recognized in import paths,
|
||||
// and avoids confusing sequences like '%20' or '+' that would change meaning
|
||||
// if used in a URL.
|
||||
//
|
||||
// TODO(rsc): We would like to allow Unicode letters, but that requires additional
|
||||
// care in the safe encoding (see "escaped paths" above).
|
||||
func modPathOK(r rune) bool {
|
||||
if r < utf8.RuneSelf {
|
||||
return r == '-' || r == '.' || r == '_' || r == '~' ||
|
||||
'0' <= r && r <= '9' ||
|
||||
'A' <= r && r <= 'Z' ||
|
||||
'a' <= r && r <= 'z'
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// importPathOK reports whether r can appear in a package import path element.
|
||||
//
|
||||
// Import paths are intermediate between module paths and file paths: we allow
|
||||
// disallow characters that would be confusing or ambiguous as arguments to
|
||||
// 'go get' (such as '@' and ' ' ), but allow certain characters that are
|
||||
// otherwise-unambiguous on the command line and historically used for some
|
||||
// binary names (such as '++' as a suffix for compiler binaries and wrappers).
|
||||
func importPathOK(r rune) bool {
|
||||
return modPathOK(r) || r == '+'
|
||||
}
|
||||
|
||||
// fileNameOK reports whether r can appear in a file name.
|
||||
// For now we allow all Unicode letters but otherwise limit to pathOK plus a few more punctuation characters.
|
||||
// If we expand the set of allowed characters here, we have to
|
||||
// work harder at detecting potential case-folding and normalization collisions.
|
||||
// See note about "escaped paths" above.
|
||||
func fileNameOK(r rune) bool {
|
||||
if r < utf8.RuneSelf {
|
||||
// Entire set of ASCII punctuation, from which we remove characters:
|
||||
// ! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ \ ] ^ _ ` { | } ~
|
||||
// We disallow some shell special characters: " ' * < > ? ` |
|
||||
// (Note that some of those are disallowed by the Windows file system as well.)
|
||||
// We also disallow path separators / : and \ (fileNameOK is only called on path element characters).
|
||||
// We allow spaces (U+0020) in file names.
|
||||
const allowed = "!#$%&()+,-.=@[]^_{}~ "
|
||||
if '0' <= r && r <= '9' || 'A' <= r && r <= 'Z' || 'a' <= r && r <= 'z' {
|
||||
return true
|
||||
}
|
||||
return strings.ContainsRune(allowed, r)
|
||||
}
|
||||
// It may be OK to add more ASCII punctuation here, but only carefully.
|
||||
// For example Windows disallows < > \, and macOS disallows :, so we must not allow those.
|
||||
return unicode.IsLetter(r)
|
||||
}
|
||||
|
||||
// CheckPath checks that a module path is valid.
|
||||
// A valid module path is a valid import path, as checked by [CheckImportPath],
|
||||
// with three additional constraints.
|
||||
// First, the leading path element (up to the first slash, if any),
|
||||
// by convention a domain name, must contain only lower-case ASCII letters,
|
||||
// ASCII digits, dots (U+002E), and dashes (U+002D);
|
||||
// it must contain at least one dot and cannot start with a dash.
|
||||
// Second, for a final path element of the form /vN, where N looks numeric
|
||||
// (ASCII digits and dots) must not begin with a leading zero, must not be /v1,
|
||||
// and must not contain any dots. For paths beginning with "gopkg.in/",
|
||||
// this second requirement is replaced by a requirement that the path
|
||||
// follow the gopkg.in server's conventions.
|
||||
// Third, no path element may begin with a dot.
|
||||
func CheckPath(path string) (err error) {
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = &InvalidPathError{Kind: "module", Path: path, Err: err}
|
||||
}
|
||||
}()
|
||||
|
||||
if err := checkPath(path, modulePath); err != nil {
|
||||
return err
|
||||
}
|
||||
i := strings.Index(path, "/")
|
||||
if i < 0 {
|
||||
i = len(path)
|
||||
}
|
||||
if i == 0 {
|
||||
return fmt.Errorf("leading slash")
|
||||
}
|
||||
if !strings.Contains(path[:i], ".") {
|
||||
return fmt.Errorf("missing dot in first path element")
|
||||
}
|
||||
if path[0] == '-' {
|
||||
return fmt.Errorf("leading dash in first path element")
|
||||
}
|
||||
for _, r := range path[:i] {
|
||||
if !firstPathOK(r) {
|
||||
return fmt.Errorf("invalid char %q in first path element", r)
|
||||
}
|
||||
}
|
||||
if _, _, ok := SplitPathVersion(path); !ok {
|
||||
return fmt.Errorf("invalid version")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckImportPath checks that an import path is valid.
|
||||
//
|
||||
// A valid import path consists of one or more valid path elements
|
||||
// separated by slashes (U+002F). (It must not begin with nor end in a slash.)
|
||||
//
|
||||
// A valid path element is a non-empty string made up of
|
||||
// ASCII letters, ASCII digits, and limited ASCII punctuation: - . _ and ~.
|
||||
// It must not end with a dot (U+002E), nor contain two dots in a row.
|
||||
//
|
||||
// The element prefix up to the first dot must not be a reserved file name
|
||||
// on Windows, regardless of case (CON, com1, NuL, and so on). The element
|
||||
// must not have a suffix of a tilde followed by one or more ASCII digits
|
||||
// (to exclude paths elements that look like Windows short-names).
|
||||
//
|
||||
// CheckImportPath may be less restrictive in the future, but see the
|
||||
// top-level package documentation for additional information about
|
||||
// subtleties of Unicode.
|
||||
func CheckImportPath(path string) error {
|
||||
if err := checkPath(path, importPath); err != nil {
|
||||
return &InvalidPathError{Kind: "import", Path: path, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pathKind indicates what kind of path we're checking. Module paths,
|
||||
// import paths, and file paths have different restrictions.
|
||||
type pathKind int
|
||||
|
||||
const (
|
||||
modulePath pathKind = iota
|
||||
importPath
|
||||
filePath
|
||||
)
|
||||
|
||||
// checkPath checks that a general path is valid. kind indicates what
|
||||
// specific constraints should be applied.
|
||||
//
|
||||
// checkPath returns an error describing why the path is not valid.
|
||||
// Because these checks apply to module, import, and file paths,
|
||||
// and because other checks may be applied, the caller is expected to wrap
|
||||
// this error with [InvalidPathError].
|
||||
func checkPath(path string, kind pathKind) error {
|
||||
if !utf8.ValidString(path) {
|
||||
return fmt.Errorf("invalid UTF-8")
|
||||
}
|
||||
if path == "" {
|
||||
return fmt.Errorf("empty string")
|
||||
}
|
||||
if path[0] == '-' && kind != filePath {
|
||||
return fmt.Errorf("leading dash")
|
||||
}
|
||||
if strings.Contains(path, "//") {
|
||||
return fmt.Errorf("double slash")
|
||||
}
|
||||
if path[len(path)-1] == '/' {
|
||||
return fmt.Errorf("trailing slash")
|
||||
}
|
||||
elemStart := 0
|
||||
for i, r := range path {
|
||||
if r == '/' {
|
||||
if err := checkElem(path[elemStart:i], kind); err != nil {
|
||||
return err
|
||||
}
|
||||
elemStart = i + 1
|
||||
}
|
||||
}
|
||||
if err := checkElem(path[elemStart:], kind); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkElem checks whether an individual path element is valid.
|
||||
func checkElem(elem string, kind pathKind) error {
|
||||
if elem == "" {
|
||||
return fmt.Errorf("empty path element")
|
||||
}
|
||||
if strings.Count(elem, ".") == len(elem) {
|
||||
return fmt.Errorf("invalid path element %q", elem)
|
||||
}
|
||||
if elem[0] == '.' && kind == modulePath {
|
||||
return fmt.Errorf("leading dot in path element")
|
||||
}
|
||||
if elem[len(elem)-1] == '.' {
|
||||
return fmt.Errorf("trailing dot in path element")
|
||||
}
|
||||
for _, r := range elem {
|
||||
ok := false
|
||||
switch kind {
|
||||
case modulePath:
|
||||
ok = modPathOK(r)
|
||||
case importPath:
|
||||
ok = importPathOK(r)
|
||||
case filePath:
|
||||
ok = fileNameOK(r)
|
||||
default:
|
||||
panic(fmt.Sprintf("internal error: invalid kind %v", kind))
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid char %q", r)
|
||||
}
|
||||
}
|
||||
|
||||
// Windows disallows a bunch of path elements, sadly.
|
||||
// See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
|
||||
short := elem
|
||||
if i := strings.Index(short, "."); i >= 0 {
|
||||
short = short[:i]
|
||||
}
|
||||
for _, bad := range badWindowsNames {
|
||||
if strings.EqualFold(bad, short) {
|
||||
return fmt.Errorf("%q disallowed as path element component on Windows", short)
|
||||
}
|
||||
}
|
||||
|
||||
if kind == filePath {
|
||||
// don't check for Windows short-names in file names. They're
|
||||
// only an issue for import paths.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reject path components that look like Windows short-names.
|
||||
// Those usually end in a tilde followed by one or more ASCII digits.
|
||||
if tilde := strings.LastIndexByte(short, '~'); tilde >= 0 && tilde < len(short)-1 {
|
||||
suffix := short[tilde+1:]
|
||||
suffixIsDigits := true
|
||||
for _, r := range suffix {
|
||||
if r < '0' || r > '9' {
|
||||
suffixIsDigits = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if suffixIsDigits {
|
||||
return fmt.Errorf("trailing tilde and digits in path element")
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckFilePath checks that a slash-separated file path is valid.
|
||||
// The definition of a valid file path is the same as the definition
|
||||
// of a valid import path except that the set of allowed characters is larger:
|
||||
// all Unicode letters, ASCII digits, the ASCII space character (U+0020),
|
||||
// and the ASCII punctuation characters
|
||||
// “!#$%&()+,-.=@[]^_{}~”.
|
||||
// (The excluded punctuation characters, " * < > ? ` ' | / \ and :,
|
||||
// have special meanings in certain shells or operating systems.)
|
||||
//
|
||||
// CheckFilePath may be less restrictive in the future, but see the
|
||||
// top-level package documentation for additional information about
|
||||
// subtleties of Unicode.
|
||||
func CheckFilePath(path string) error {
|
||||
if err := checkPath(path, filePath); err != nil {
|
||||
return &InvalidPathError{Kind: "file", Path: path, Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// badWindowsNames are the reserved file path elements on Windows.
|
||||
// See https://docs.microsoft.com/en-us/windows/desktop/fileio/naming-a-file
|
||||
var badWindowsNames = []string{
|
||||
"CON",
|
||||
"PRN",
|
||||
"AUX",
|
||||
"NUL",
|
||||
"COM1",
|
||||
"COM2",
|
||||
"COM3",
|
||||
"COM4",
|
||||
"COM5",
|
||||
"COM6",
|
||||
"COM7",
|
||||
"COM8",
|
||||
"COM9",
|
||||
"LPT1",
|
||||
"LPT2",
|
||||
"LPT3",
|
||||
"LPT4",
|
||||
"LPT5",
|
||||
"LPT6",
|
||||
"LPT7",
|
||||
"LPT8",
|
||||
"LPT9",
|
||||
}
|
||||
|
||||
// SplitPathVersion returns prefix and major version such that prefix+pathMajor == path
|
||||
// and version is either empty or "/vN" for N >= 2.
|
||||
// As a special case, gopkg.in paths are recognized directly;
|
||||
// they require ".vN" instead of "/vN", and for all N, not just N >= 2.
|
||||
// SplitPathVersion returns with ok = false when presented with
|
||||
// a path whose last path element does not satisfy the constraints
|
||||
// applied by [CheckPath], such as "example.com/pkg/v1" or "example.com/pkg/v1.2".
|
||||
func SplitPathVersion(path string) (prefix, pathMajor string, ok bool) {
|
||||
if strings.HasPrefix(path, "gopkg.in/") {
|
||||
return splitGopkgIn(path)
|
||||
}
|
||||
|
||||
i := len(path)
|
||||
dot := false
|
||||
for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9' || path[i-1] == '.') {
|
||||
if path[i-1] == '.' {
|
||||
dot = true
|
||||
}
|
||||
i--
|
||||
}
|
||||
if i <= 1 || i == len(path) || path[i-1] != 'v' || path[i-2] != '/' {
|
||||
return path, "", true
|
||||
}
|
||||
prefix, pathMajor = path[:i-2], path[i-2:]
|
||||
if dot || len(pathMajor) <= 2 || pathMajor[2] == '0' || pathMajor == "/v1" {
|
||||
return path, "", false
|
||||
}
|
||||
return prefix, pathMajor, true
|
||||
}
|
||||
|
||||
// splitGopkgIn is like SplitPathVersion but only for gopkg.in paths.
|
||||
func splitGopkgIn(path string) (prefix, pathMajor string, ok bool) {
|
||||
if !strings.HasPrefix(path, "gopkg.in/") {
|
||||
return path, "", false
|
||||
}
|
||||
i := len(path)
|
||||
if strings.HasSuffix(path, "-unstable") {
|
||||
i -= len("-unstable")
|
||||
}
|
||||
for i > 0 && ('0' <= path[i-1] && path[i-1] <= '9') {
|
||||
i--
|
||||
}
|
||||
if i <= 1 || path[i-1] != 'v' || path[i-2] != '.' {
|
||||
// All gopkg.in paths must end in vN for some N.
|
||||
return path, "", false
|
||||
}
|
||||
prefix, pathMajor = path[:i-2], path[i-2:]
|
||||
if len(pathMajor) <= 2 || pathMajor[2] == '0' && pathMajor != ".v0" {
|
||||
return path, "", false
|
||||
}
|
||||
return prefix, pathMajor, true
|
||||
}
|
||||
|
||||
// MatchPathMajor reports whether the semantic version v
|
||||
// matches the path major version pathMajor.
|
||||
//
|
||||
// MatchPathMajor returns true if and only if [CheckPathMajor] returns nil.
|
||||
func MatchPathMajor(v, pathMajor string) bool {
|
||||
return CheckPathMajor(v, pathMajor) == nil
|
||||
}
|
||||
|
||||
// CheckPathMajor returns a non-nil error if the semantic version v
|
||||
// does not match the path major version pathMajor.
|
||||
func CheckPathMajor(v, pathMajor string) error {
|
||||
// TODO(jayconrod): return errors or panic for invalid inputs. This function
|
||||
// (and others) was covered by integration tests for cmd/go, and surrounding
|
||||
// code protected against invalid inputs like non-canonical versions.
|
||||
if strings.HasPrefix(pathMajor, ".v") && strings.HasSuffix(pathMajor, "-unstable") {
|
||||
pathMajor = strings.TrimSuffix(pathMajor, "-unstable")
|
||||
}
|
||||
if strings.HasPrefix(v, "v0.0.0-") && pathMajor == ".v1" {
|
||||
// Allow old bug in pseudo-versions that generated v0.0.0- pseudoversion for gopkg .v1.
|
||||
// For example, gopkg.in/yaml.v2@v2.2.1's go.mod requires gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405.
|
||||
return nil
|
||||
}
|
||||
m := semver.Major(v)
|
||||
if pathMajor == "" {
|
||||
if m == "v0" || m == "v1" || semver.Build(v) == "+incompatible" {
|
||||
return nil
|
||||
}
|
||||
pathMajor = "v0 or v1"
|
||||
} else if pathMajor[0] == '/' || pathMajor[0] == '.' {
|
||||
if m == pathMajor[1:] {
|
||||
return nil
|
||||
}
|
||||
pathMajor = pathMajor[1:]
|
||||
}
|
||||
return &InvalidVersionError{
|
||||
Version: v,
|
||||
Err: fmt.Errorf("should be %s, not %s", pathMajor, semver.Major(v)),
|
||||
}
|
||||
}
|
||||
|
||||
// PathMajorPrefix returns the major-version tag prefix implied by pathMajor.
|
||||
// An empty PathMajorPrefix allows either v0 or v1.
|
||||
//
|
||||
// Note that [MatchPathMajor] may accept some versions that do not actually begin
|
||||
// with this prefix: namely, it accepts a 'v0.0.0-' prefix for a '.v1'
|
||||
// pathMajor, even though that pathMajor implies 'v1' tagging.
|
||||
func PathMajorPrefix(pathMajor string) string {
|
||||
if pathMajor == "" {
|
||||
return ""
|
||||
}
|
||||
if pathMajor[0] != '/' && pathMajor[0] != '.' {
|
||||
panic("pathMajor suffix " + pathMajor + " passed to PathMajorPrefix lacks separator")
|
||||
}
|
||||
if strings.HasPrefix(pathMajor, ".v") && strings.HasSuffix(pathMajor, "-unstable") {
|
||||
pathMajor = strings.TrimSuffix(pathMajor, "-unstable")
|
||||
}
|
||||
m := pathMajor[1:]
|
||||
if m != semver.Major(m) {
|
||||
panic("pathMajor suffix " + pathMajor + "passed to PathMajorPrefix is not a valid major version")
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// CanonicalVersion returns the canonical form of the version string v.
|
||||
// It is the same as [semver.Canonical] except that it preserves the special build suffix "+incompatible".
|
||||
func CanonicalVersion(v string) string {
|
||||
cv := semver.Canonical(v)
|
||||
if semver.Build(v) == "+incompatible" {
|
||||
cv += "+incompatible"
|
||||
}
|
||||
return cv
|
||||
}
|
||||
|
||||
// Sort sorts the list by Path, breaking ties by comparing [Version] fields.
|
||||
// The Version fields are interpreted as semantic versions (using [semver.Compare])
|
||||
// optionally followed by a tie-breaking suffix introduced by a slash character,
|
||||
// like in "v0.0.1/go.mod".
|
||||
func Sort(list []Version) {
|
||||
sort.Slice(list, func(i, j int) bool {
|
||||
mi := list[i]
|
||||
mj := list[j]
|
||||
if mi.Path != mj.Path {
|
||||
return mi.Path < mj.Path
|
||||
}
|
||||
// To help go.sum formatting, allow version/file.
|
||||
// Compare semver prefix by semver rules,
|
||||
// file by string order.
|
||||
vi := mi.Version
|
||||
vj := mj.Version
|
||||
var fi, fj string
|
||||
if k := strings.Index(vi, "/"); k >= 0 {
|
||||
vi, fi = vi[:k], vi[k:]
|
||||
}
|
||||
if k := strings.Index(vj, "/"); k >= 0 {
|
||||
vj, fj = vj[:k], vj[k:]
|
||||
}
|
||||
if vi != vj {
|
||||
return semver.Compare(vi, vj) < 0
|
||||
}
|
||||
return fi < fj
|
||||
})
|
||||
}
|
||||
|
||||
// EscapePath returns the escaped form of the given module path.
|
||||
// It fails if the module path is invalid.
|
||||
func EscapePath(path string) (escaped string, err error) {
|
||||
if err := CheckPath(path); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return escapeString(path)
|
||||
}
|
||||
|
||||
// EscapeVersion returns the escaped form of the given module version.
|
||||
// Versions are allowed to be in non-semver form but must be valid file names
|
||||
// and not contain exclamation marks.
|
||||
func EscapeVersion(v string) (escaped string, err error) {
|
||||
if err := checkElem(v, filePath); err != nil || strings.Contains(v, "!") {
|
||||
return "", &InvalidVersionError{
|
||||
Version: v,
|
||||
Err: fmt.Errorf("disallowed version string"),
|
||||
}
|
||||
}
|
||||
return escapeString(v)
|
||||
}
|
||||
|
||||
func escapeString(s string) (escaped string, err error) {
|
||||
haveUpper := false
|
||||
for _, r := range s {
|
||||
if r == '!' || r >= utf8.RuneSelf {
|
||||
// This should be disallowed by CheckPath, but diagnose anyway.
|
||||
// The correctness of the escaping loop below depends on it.
|
||||
return "", fmt.Errorf("internal error: inconsistency in EscapePath")
|
||||
}
|
||||
if 'A' <= r && r <= 'Z' {
|
||||
haveUpper = true
|
||||
}
|
||||
}
|
||||
|
||||
if !haveUpper {
|
||||
return s, nil
|
||||
}
|
||||
|
||||
var buf []byte
|
||||
for _, r := range s {
|
||||
if 'A' <= r && r <= 'Z' {
|
||||
buf = append(buf, '!', byte(r+'a'-'A'))
|
||||
} else {
|
||||
buf = append(buf, byte(r))
|
||||
}
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
// UnescapePath returns the module path for the given escaped path.
|
||||
// It fails if the escaped path is invalid or describes an invalid path.
|
||||
func UnescapePath(escaped string) (path string, err error) {
|
||||
path, ok := unescapeString(escaped)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid escaped module path %q", escaped)
|
||||
}
|
||||
if err := CheckPath(path); err != nil {
|
||||
return "", fmt.Errorf("invalid escaped module path %q: %v", escaped, err)
|
||||
}
|
||||
return path, nil
|
||||
}
|
||||
|
||||
// UnescapeVersion returns the version string for the given escaped version.
|
||||
// It fails if the escaped form is invalid or describes an invalid version.
|
||||
// Versions are allowed to be in non-semver form but must be valid file names
|
||||
// and not contain exclamation marks.
|
||||
func UnescapeVersion(escaped string) (v string, err error) {
|
||||
v, ok := unescapeString(escaped)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("invalid escaped version %q", escaped)
|
||||
}
|
||||
if err := checkElem(v, filePath); err != nil {
|
||||
return "", fmt.Errorf("invalid escaped version %q: %v", v, err)
|
||||
}
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func unescapeString(escaped string) (string, bool) {
|
||||
var buf []byte
|
||||
|
||||
bang := false
|
||||
for _, r := range escaped {
|
||||
if r >= utf8.RuneSelf {
|
||||
return "", false
|
||||
}
|
||||
if bang {
|
||||
bang = false
|
||||
if r < 'a' || 'z' < r {
|
||||
return "", false
|
||||
}
|
||||
buf = append(buf, byte(r+'A'-'a'))
|
||||
continue
|
||||
}
|
||||
if r == '!' {
|
||||
bang = true
|
||||
continue
|
||||
}
|
||||
if 'A' <= r && r <= 'Z' {
|
||||
return "", false
|
||||
}
|
||||
buf = append(buf, byte(r))
|
||||
}
|
||||
if bang {
|
||||
return "", false
|
||||
}
|
||||
return string(buf), true
|
||||
}
|
||||
|
||||
// MatchPrefixPatterns reports whether any path prefix of target matches one of
|
||||
// the glob patterns (as defined by [path.Match]) in the comma-separated globs
|
||||
// list. This implements the algorithm used when matching a module path to the
|
||||
// GOPRIVATE environment variable, as described by 'go help module-private'.
|
||||
//
|
||||
// It ignores any empty or malformed patterns in the list.
|
||||
// Trailing slashes on patterns are ignored.
|
||||
func MatchPrefixPatterns(globs, target string) bool {
|
||||
for globs != "" {
|
||||
// Extract next non-empty glob in comma-separated list.
|
||||
var glob string
|
||||
if i := strings.Index(globs, ","); i >= 0 {
|
||||
glob, globs = globs[:i], globs[i+1:]
|
||||
} else {
|
||||
glob, globs = globs, ""
|
||||
}
|
||||
glob = strings.TrimSuffix(glob, "/")
|
||||
if glob == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// A glob with N+1 path elements (N slashes) needs to be matched
|
||||
// against the first N+1 path elements of target,
|
||||
// which end just before the N+1'th slash.
|
||||
n := strings.Count(glob, "/")
|
||||
prefix := target
|
||||
// Walk target, counting slashes, truncating at the N+1'th slash.
|
||||
for i := 0; i < len(target); i++ {
|
||||
if target[i] == '/' {
|
||||
if n == 0 {
|
||||
prefix = target[:i]
|
||||
break
|
||||
}
|
||||
n--
|
||||
}
|
||||
}
|
||||
if n > 0 {
|
||||
// Not enough prefix elements.
|
||||
continue
|
||||
}
|
||||
matched, _ := path.Match(glob, prefix)
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
// Copyright 2018 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 module
|
||||
|
||||
import "testing"
|
||||
|
||||
var checkTests = []struct {
|
||||
path string
|
||||
version string
|
||||
ok bool
|
||||
}{
|
||||
{"rsc.io/quote", "0.1.0", false},
|
||||
{"rsc io/quote", "v1.0.0", false},
|
||||
|
||||
{"github.com/go-yaml/yaml", "v0.8.0", true},
|
||||
{"github.com/go-yaml/yaml", "v1.0.0", true},
|
||||
{"github.com/go-yaml/yaml", "v2.0.0", false},
|
||||
{"github.com/go-yaml/yaml", "v2.1.5", false},
|
||||
{"github.com/go-yaml/yaml", "v3.0.0", false},
|
||||
|
||||
{"github.com/go-yaml/yaml/v2", "v1.0.0", false},
|
||||
{"github.com/go-yaml/yaml/v2", "v2.0.0", true},
|
||||
{"github.com/go-yaml/yaml/v2", "v2.1.5", true},
|
||||
{"github.com/go-yaml/yaml/v2", "v3.0.0", false},
|
||||
|
||||
{"gopkg.in/yaml.v0", "v0.8.0", true},
|
||||
{"gopkg.in/yaml.v0", "v1.0.0", false},
|
||||
{"gopkg.in/yaml.v0", "v2.0.0", false},
|
||||
{"gopkg.in/yaml.v0", "v2.1.5", false},
|
||||
{"gopkg.in/yaml.v0", "v3.0.0", false},
|
||||
|
||||
{"gopkg.in/yaml.v1", "v0.8.0", false},
|
||||
{"gopkg.in/yaml.v1", "v1.0.0", true},
|
||||
{"gopkg.in/yaml.v1", "v2.0.0", false},
|
||||
{"gopkg.in/yaml.v1", "v2.1.5", false},
|
||||
{"gopkg.in/yaml.v1", "v3.0.0", false},
|
||||
|
||||
// For gopkg.in, .v1 means v1 only (not v0).
|
||||
// But early versions of vgo still generated v0 pseudo-versions for it.
|
||||
// Even though now we'd generate those as v1 pseudo-versions,
|
||||
// we accept the old pseudo-versions to avoid breaking existing go.mod files.
|
||||
// For example gopkg.in/yaml.v2@v2.2.1's go.mod requires check.v1 at a v0 pseudo-version.
|
||||
{"gopkg.in/check.v1", "v0.0.0", false},
|
||||
{"gopkg.in/check.v1", "v0.0.0-20160102150405-abcdef123456", true},
|
||||
|
||||
{"gopkg.in/yaml.v2", "v1.0.0", false},
|
||||
{"gopkg.in/yaml.v2", "v2.0.0", true},
|
||||
{"gopkg.in/yaml.v2", "v2.1.5", true},
|
||||
{"gopkg.in/yaml.v2", "v3.0.0", false},
|
||||
|
||||
{"rsc.io/quote", "v17.0.0", false},
|
||||
{"rsc.io/quote", "v17.0.0+incompatible", true},
|
||||
}
|
||||
|
||||
func TestCheck(t *testing.T) {
|
||||
for _, tt := range checkTests {
|
||||
err := Check(tt.path, tt.version)
|
||||
if tt.ok && err != nil {
|
||||
t.Errorf("Check(%q, %q) = %v, wanted nil error", tt.path, tt.version, err)
|
||||
} else if !tt.ok && err == nil {
|
||||
t.Errorf("Check(%q, %q) succeeded, wanted error", tt.path, tt.version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var checkPathTests = []struct {
|
||||
path string
|
||||
ok bool
|
||||
importOK bool
|
||||
fileOK bool
|
||||
}{
|
||||
{"x.y/z", true, true, true},
|
||||
{"x.y", true, true, true},
|
||||
|
||||
{"", false, false, false},
|
||||
{"x.y/\xFFz", false, false, false},
|
||||
{"/x.y/z", false, false, false},
|
||||
{"x./z", false, false, false},
|
||||
{".x/z", false, true, true},
|
||||
{"-x/z", false, false, true},
|
||||
{"x..y/z", true, true, true},
|
||||
{"x.y/z/../../w", false, false, false},
|
||||
{"x.y//z", false, false, false},
|
||||
{"x.y/z//w", false, false, false},
|
||||
{"x.y/z/", false, false, false},
|
||||
|
||||
{"x.y/z/v0", false, true, true},
|
||||
{"x.y/z/v1", false, true, true},
|
||||
{"x.y/z/v2", true, true, true},
|
||||
{"x.y/z/v2.0", false, true, true},
|
||||
{"X.y/z", false, true, true},
|
||||
|
||||
{"!x.y/z", false, false, true},
|
||||
{"_x.y/z", false, true, true},
|
||||
{"x.y!/z", false, false, true},
|
||||
{"x.y\"/z", false, false, false},
|
||||
{"x.y#/z", false, false, true},
|
||||
{"x.y$/z", false, false, true},
|
||||
{"x.y%/z", false, false, true},
|
||||
{"x.y&/z", false, false, true},
|
||||
{"x.y'/z", false, false, false},
|
||||
{"x.y(/z", false, false, true},
|
||||
{"x.y)/z", false, false, true},
|
||||
{"x.y*/z", false, false, false},
|
||||
{"x.y+/z", false, true, true},
|
||||
{"x.y,/z", false, false, true},
|
||||
{"x.y-/z", true, true, true},
|
||||
{"x.y./zt", false, false, false},
|
||||
{"x.y:/z", false, false, false},
|
||||
{"x.y;/z", false, false, false},
|
||||
{"x.y</z", false, false, false},
|
||||
{"x.y=/z", false, false, true},
|
||||
{"x.y>/z", false, false, false},
|
||||
{"x.y?/z", false, false, false},
|
||||
{"x.y@/z", false, false, true},
|
||||
{"x.y[/z", false, false, true},
|
||||
{"x.y\\/z", false, false, false},
|
||||
{"x.y]/z", false, false, true},
|
||||
{"x.y^/z", false, false, true},
|
||||
{"x.y_/z", false, true, true},
|
||||
{"x.y`/z", false, false, false},
|
||||
{"x.y{/z", false, false, true},
|
||||
{"x.y}/z", false, false, true},
|
||||
{"x.y~/z", false, true, true},
|
||||
{"x.y/z!", false, false, true},
|
||||
{"x.y/z\"", false, false, false},
|
||||
{"x.y/z#", false, false, true},
|
||||
{"x.y/z$", false, false, true},
|
||||
{"x.y/z%", false, false, true},
|
||||
{"x.y/z&", false, false, true},
|
||||
{"x.y/z'", false, false, false},
|
||||
{"x.y/z(", false, false, true},
|
||||
{"x.y/z)", false, false, true},
|
||||
{"x.y/z*", false, false, false},
|
||||
{"x.y/z++", false, true, true},
|
||||
{"x.y/z,", false, false, true},
|
||||
{"x.y/z-", true, true, true},
|
||||
{"x.y/z.t", true, true, true},
|
||||
{"x.y/z/t", true, true, true},
|
||||
{"x.y/z:", false, false, false},
|
||||
{"x.y/z;", false, false, false},
|
||||
{"x.y/z<", false, false, false},
|
||||
{"x.y/z=", false, false, true},
|
||||
{"x.y/z>", false, false, false},
|
||||
{"x.y/z?", false, false, false},
|
||||
{"x.y/z@", false, false, true},
|
||||
{"x.y/z[", false, false, true},
|
||||
{"x.y/z\\", false, false, false},
|
||||
{"x.y/z]", false, false, true},
|
||||
{"x.y/z^", false, false, true},
|
||||
{"x.y/z_", true, true, true},
|
||||
{"x.y/z`", false, false, false},
|
||||
{"x.y/z{", false, false, true},
|
||||
{"x.y/z}", false, false, true},
|
||||
{"x.y/z~", true, true, true},
|
||||
{"x.y/x.foo", true, true, true},
|
||||
{"x.y/aux.foo", false, false, false},
|
||||
{"x.y/prn", false, false, false},
|
||||
{"x.y/prn2", true, true, true},
|
||||
{"x.y/com", true, true, true},
|
||||
{"x.y/com1", false, false, false},
|
||||
{"x.y/com1.txt", false, false, false},
|
||||
{"x.y/calm1", true, true, true},
|
||||
{"x.y/z~", true, true, true},
|
||||
{"x.y/z~0", false, false, true},
|
||||
{"x.y/z~09", false, false, true},
|
||||
{"x.y/z09", true, true, true},
|
||||
{"x.y/z09~", true, true, true},
|
||||
{"x.y/z09~09z", true, true, true},
|
||||
{"x.y/z09~09z~09", false, false, true},
|
||||
{"github.com/!123/logrus", false, false, true},
|
||||
|
||||
// TODO: CL 41822 allowed Unicode letters in old "go get"
|
||||
// without due consideration of the implications, and only on github.com (!).
|
||||
// For now, we disallow non-ASCII characters in module mode,
|
||||
// in both module paths and general import paths,
|
||||
// until we can get the implications right.
|
||||
// When we do, we'll enable them everywhere, not just for GitHub.
|
||||
{"github.com/user/unicode/испытание", false, false, true},
|
||||
|
||||
{"../x", false, false, false},
|
||||
{"./y", false, false, false},
|
||||
{"x:y", false, false, false},
|
||||
{`\temp\foo`, false, false, false},
|
||||
{".gitignore", false, true, true},
|
||||
{".github/ISSUE_TEMPLATE", false, true, true},
|
||||
{"x☺y", false, false, false},
|
||||
}
|
||||
|
||||
func TestCheckPath(t *testing.T) {
|
||||
for _, tt := range checkPathTests {
|
||||
err := CheckPath(tt.path)
|
||||
if tt.ok && err != nil {
|
||||
t.Errorf("CheckPath(%q) = %v, wanted nil error", tt.path, err)
|
||||
} else if !tt.ok && err == nil {
|
||||
t.Errorf("CheckPath(%q) succeeded, wanted error", tt.path)
|
||||
}
|
||||
|
||||
err = CheckImportPath(tt.path)
|
||||
if tt.importOK && err != nil {
|
||||
t.Errorf("CheckImportPath(%q) = %v, wanted nil error", tt.path, err)
|
||||
} else if !tt.importOK && err == nil {
|
||||
t.Errorf("CheckImportPath(%q) succeeded, wanted error", tt.path)
|
||||
}
|
||||
|
||||
err = CheckFilePath(tt.path)
|
||||
if tt.fileOK && err != nil {
|
||||
t.Errorf("CheckFilePath(%q) = %v, wanted nil error", tt.path, err)
|
||||
} else if !tt.fileOK && err == nil {
|
||||
t.Errorf("CheckFilePath(%q) succeeded, wanted error", tt.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var splitPathVersionTests = []struct {
|
||||
pathPrefix string
|
||||
version string
|
||||
}{
|
||||
{"x.y/z", ""},
|
||||
{"x.y/z", "/v2"},
|
||||
{"x.y/z", "/v3"},
|
||||
{"x.y/v", ""},
|
||||
{"gopkg.in/yaml", ".v0"},
|
||||
{"gopkg.in/yaml", ".v1"},
|
||||
{"gopkg.in/yaml", ".v2"},
|
||||
{"gopkg.in/yaml", ".v3"},
|
||||
}
|
||||
|
||||
func TestSplitPathVersion(t *testing.T) {
|
||||
for _, tt := range splitPathVersionTests {
|
||||
pathPrefix, version, ok := SplitPathVersion(tt.pathPrefix + tt.version)
|
||||
if pathPrefix != tt.pathPrefix || version != tt.version || !ok {
|
||||
t.Errorf("SplitPathVersion(%q) = %q, %q, %v, want %q, %q, true", tt.pathPrefix+tt.version, pathPrefix, version, ok, tt.pathPrefix, tt.version)
|
||||
}
|
||||
}
|
||||
|
||||
for _, tt := range checkPathTests {
|
||||
pathPrefix, version, ok := SplitPathVersion(tt.path)
|
||||
if pathPrefix+version != tt.path {
|
||||
t.Errorf("SplitPathVersion(%q) = %q, %q, %v, doesn't add to input", tt.path, pathPrefix, version, ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var escapeTests = []struct {
|
||||
path string
|
||||
esc string // empty means same as path
|
||||
}{
|
||||
{path: "ascii.com/abcdefghijklmnopqrstuvwxyz.-/~_0123456789"},
|
||||
{path: "github.com/GoogleCloudPlatform/omega", esc: "github.com/!google!cloud!platform/omega"},
|
||||
}
|
||||
|
||||
func TestEscapePath(t *testing.T) {
|
||||
// Check invalid paths.
|
||||
for _, tt := range checkPathTests {
|
||||
if !tt.ok {
|
||||
_, err := EscapePath(tt.path)
|
||||
if err == nil {
|
||||
t.Errorf("EscapePath(%q): succeeded, want error (invalid path)", tt.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check encodings.
|
||||
for _, tt := range escapeTests {
|
||||
esc, err := EscapePath(tt.path)
|
||||
if err != nil {
|
||||
t.Errorf("EscapePath(%q): unexpected error: %v", tt.path, err)
|
||||
continue
|
||||
}
|
||||
want := tt.esc
|
||||
if want == "" {
|
||||
want = tt.path
|
||||
}
|
||||
if esc != want {
|
||||
t.Errorf("EscapePath(%q) = %q, want %q", tt.path, esc, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var badUnescape = []string{
|
||||
"github.com/GoogleCloudPlatform/omega",
|
||||
"github.com/!google!cloud!platform!/omega",
|
||||
"github.com/!0google!cloud!platform/omega",
|
||||
"github.com/!_google!cloud!platform/omega",
|
||||
"github.com/!!google!cloud!platform/omega",
|
||||
"",
|
||||
}
|
||||
|
||||
func TestUnescapePath(t *testing.T) {
|
||||
// Check invalid decodings.
|
||||
for _, bad := range badUnescape {
|
||||
_, err := UnescapePath(bad)
|
||||
if err == nil {
|
||||
t.Errorf("UnescapePath(%q): succeeded, want error (invalid decoding)", bad)
|
||||
}
|
||||
}
|
||||
|
||||
// Check invalid paths (or maybe decodings).
|
||||
for _, tt := range checkPathTests {
|
||||
if !tt.ok {
|
||||
path, err := UnescapePath(tt.path)
|
||||
if err == nil {
|
||||
t.Errorf("UnescapePath(%q) = %q, want error (invalid path)", tt.path, path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check encodings.
|
||||
for _, tt := range escapeTests {
|
||||
esc := tt.esc
|
||||
if esc == "" {
|
||||
esc = tt.path
|
||||
}
|
||||
path, err := UnescapePath(esc)
|
||||
if err != nil {
|
||||
t.Errorf("UnescapePath(%q): unexpected error: %v", esc, err)
|
||||
continue
|
||||
}
|
||||
if path != tt.path {
|
||||
t.Errorf("UnescapePath(%q) = %q, want %q", esc, path, tt.path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchPathMajor(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
v, pathMajor string
|
||||
want bool
|
||||
}{
|
||||
{"v0.0.0", "", true},
|
||||
{"v0.0.0", "/v2", false},
|
||||
{"v0.0.0", ".v0", true},
|
||||
{"v0.0.0-20190510104115-cbcb75029529", ".v1", true},
|
||||
{"v1.0.0", "/v2", false},
|
||||
{"v1.0.0", ".v1", true},
|
||||
{"v1.0.0", ".v1-unstable", true},
|
||||
{"v2.0.0+incompatible", "", true},
|
||||
{"v2.0.0", "", false},
|
||||
{"v2.0.0", "/v2", true},
|
||||
{"v2.0.0", ".v2", true},
|
||||
} {
|
||||
if got := MatchPathMajor(test.v, test.pathMajor); got != test.want {
|
||||
t.Errorf("MatchPathMajor(%q, %q) = %v, want %v", test.v, test.pathMajor, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatchPrefixPatterns(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
globs, target string
|
||||
want bool
|
||||
}{
|
||||
{"", "rsc.io/quote", false},
|
||||
{"/", "rsc.io/quote", false},
|
||||
{"*/quote", "rsc.io/quote", true},
|
||||
{"*/quo", "rsc.io/quote", false},
|
||||
{"*/quo??", "rsc.io/quote", true},
|
||||
{"*/quo*", "rsc.io/quote", true},
|
||||
{"*quo*", "rsc.io/quote", false},
|
||||
{"rsc.io", "rsc.io/quote", true},
|
||||
{"*.io", "rsc.io/quote", true},
|
||||
{"rsc.io/", "rsc.io/quote", true},
|
||||
{"rsc", "rsc.io/quote", false},
|
||||
{"rsc*", "rsc.io/quote", true},
|
||||
|
||||
{"rsc.io", "rsc.io/quote/v3", true},
|
||||
{"*/quote", "rsc.io/quote/v3", true},
|
||||
{"*/quote/", "rsc.io/quote/v3", true},
|
||||
{"*/quote/*", "rsc.io/quote/v3", true},
|
||||
{"*/quote/*/", "rsc.io/quote/v3", true},
|
||||
{"*/v3", "rsc.io/quote/v3", false},
|
||||
{"*/*/v3", "rsc.io/quote/v3", true},
|
||||
{"*/*/*", "rsc.io/quote/v3", true},
|
||||
{"*/*/*/", "rsc.io/quote/v3", true},
|
||||
{"*/*/*", "rsc.io/quote", false},
|
||||
{"*/*/*/", "rsc.io/quote", false},
|
||||
|
||||
{"*/*/*,,", "rsc.io/quote", false},
|
||||
{"*/*/*,,*/quote", "rsc.io/quote", true},
|
||||
{",,*/quote", "rsc.io/quote", true},
|
||||
} {
|
||||
if got := MatchPrefixPatterns(test.globs, test.target); got != test.want {
|
||||
t.Errorf("MatchPrefixPatterns(%q, %q) = %t, want %t", test.globs, test.target, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
// Copyright 2018 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.
|
||||
|
||||
// Pseudo-versions
|
||||
//
|
||||
// Code authors are expected to tag the revisions they want users to use,
|
||||
// including prereleases. However, not all authors tag versions at all,
|
||||
// and not all commits a user might want to try will have tags.
|
||||
// A pseudo-version is a version with a special form that allows us to
|
||||
// address an untagged commit and order that version with respect to
|
||||
// other versions we might encounter.
|
||||
//
|
||||
// A pseudo-version takes one of the general forms:
|
||||
//
|
||||
// (1) vX.0.0-yyyymmddhhmmss-abcdef123456
|
||||
// (2) vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456
|
||||
// (3) vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456+incompatible
|
||||
// (4) vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456
|
||||
// (5) vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456+incompatible
|
||||
//
|
||||
// If there is no recently tagged version with the right major version vX,
|
||||
// then form (1) is used, creating a space of pseudo-versions at the bottom
|
||||
// of the vX version range, less than any tagged version, including the unlikely v0.0.0.
|
||||
//
|
||||
// If the most recent tagged version before the target commit is vX.Y.Z or vX.Y.Z+incompatible,
|
||||
// then the pseudo-version uses form (2) or (3), making it a prerelease for the next
|
||||
// possible semantic version after vX.Y.Z. The leading 0 segment in the prerelease string
|
||||
// ensures that the pseudo-version compares less than possible future explicit prereleases
|
||||
// like vX.Y.(Z+1)-rc1 or vX.Y.(Z+1)-1.
|
||||
//
|
||||
// If the most recent tagged version before the target commit is vX.Y.Z-pre or vX.Y.Z-pre+incompatible,
|
||||
// then the pseudo-version uses form (4) or (5), making it a slightly later prerelease.
|
||||
|
||||
package module
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/mod/internal/lazyregexp"
|
||||
"golang.org/x/mod/semver"
|
||||
)
|
||||
|
||||
var pseudoVersionRE = lazyregexp.New(`^v[0-9]+\.(0\.0-|\d+\.\d+-([^+]*\.)?0\.)\d{14}-[A-Za-z0-9]+(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$`)
|
||||
|
||||
const PseudoVersionTimestampFormat = "20060102150405"
|
||||
|
||||
// PseudoVersion returns a pseudo-version for the given major version ("v1")
|
||||
// preexisting older tagged version ("" or "v1.2.3" or "v1.2.3-pre"), revision time,
|
||||
// and revision identifier (usually a 12-byte commit hash prefix).
|
||||
func PseudoVersion(major, older string, t time.Time, rev string) string {
|
||||
if major == "" {
|
||||
major = "v0"
|
||||
}
|
||||
segment := fmt.Sprintf("%s-%s", t.UTC().Format(PseudoVersionTimestampFormat), rev)
|
||||
build := semver.Build(older)
|
||||
older = semver.Canonical(older)
|
||||
if older == "" {
|
||||
return major + ".0.0-" + segment // form (1)
|
||||
}
|
||||
if semver.Prerelease(older) != "" {
|
||||
return older + ".0." + segment + build // form (4), (5)
|
||||
}
|
||||
|
||||
// Form (2), (3).
|
||||
// Extract patch from vMAJOR.MINOR.PATCH
|
||||
i := strings.LastIndex(older, ".") + 1
|
||||
v, patch := older[:i], older[i:]
|
||||
|
||||
// Reassemble.
|
||||
return v + incDecimal(patch) + "-0." + segment + build
|
||||
}
|
||||
|
||||
// ZeroPseudoVersion returns a pseudo-version with a zero timestamp and
|
||||
// revision, which may be used as a placeholder.
|
||||
func ZeroPseudoVersion(major string) string {
|
||||
return PseudoVersion(major, "", time.Time{}, "000000000000")
|
||||
}
|
||||
|
||||
// incDecimal returns the decimal string incremented by 1.
|
||||
func incDecimal(decimal string) string {
|
||||
// Scan right to left turning 9s to 0s until you find a digit to increment.
|
||||
digits := []byte(decimal)
|
||||
i := len(digits) - 1
|
||||
for ; i >= 0 && digits[i] == '9'; i-- {
|
||||
digits[i] = '0'
|
||||
}
|
||||
if i >= 0 {
|
||||
digits[i]++
|
||||
} else {
|
||||
// digits is all zeros
|
||||
digits[0] = '1'
|
||||
digits = append(digits, '0')
|
||||
}
|
||||
return string(digits)
|
||||
}
|
||||
|
||||
// decDecimal returns the decimal string decremented by 1, or the empty string
|
||||
// if the decimal is all zeroes.
|
||||
func decDecimal(decimal string) string {
|
||||
// Scan right to left turning 0s to 9s until you find a digit to decrement.
|
||||
digits := []byte(decimal)
|
||||
i := len(digits) - 1
|
||||
for ; i >= 0 && digits[i] == '0'; i-- {
|
||||
digits[i] = '9'
|
||||
}
|
||||
if i < 0 {
|
||||
// decimal is all zeros
|
||||
return ""
|
||||
}
|
||||
if i == 0 && digits[i] == '1' && len(digits) > 1 {
|
||||
digits = digits[1:]
|
||||
} else {
|
||||
digits[i]--
|
||||
}
|
||||
return string(digits)
|
||||
}
|
||||
|
||||
// IsPseudoVersion reports whether v is a pseudo-version.
|
||||
func IsPseudoVersion(v string) bool {
|
||||
return strings.Count(v, "-") >= 2 && semver.IsValid(v) && pseudoVersionRE.MatchString(v)
|
||||
}
|
||||
|
||||
// IsZeroPseudoVersion returns whether v is a pseudo-version with a zero base,
|
||||
// timestamp, and revision, as returned by [ZeroPseudoVersion].
|
||||
func IsZeroPseudoVersion(v string) bool {
|
||||
return v == ZeroPseudoVersion(semver.Major(v))
|
||||
}
|
||||
|
||||
// PseudoVersionTime returns the time stamp of the pseudo-version v.
|
||||
// It returns an error if v is not a pseudo-version or if the time stamp
|
||||
// embedded in the pseudo-version is not a valid time.
|
||||
func PseudoVersionTime(v string) (time.Time, error) {
|
||||
_, timestamp, _, _, err := parsePseudoVersion(v)
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
t, err := time.Parse("20060102150405", timestamp)
|
||||
if err != nil {
|
||||
return time.Time{}, &InvalidVersionError{
|
||||
Version: v,
|
||||
Pseudo: true,
|
||||
Err: fmt.Errorf("malformed time %q", timestamp),
|
||||
}
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// PseudoVersionRev returns the revision identifier of the pseudo-version v.
|
||||
// It returns an error if v is not a pseudo-version.
|
||||
func PseudoVersionRev(v string) (rev string, err error) {
|
||||
_, _, rev, _, err = parsePseudoVersion(v)
|
||||
return
|
||||
}
|
||||
|
||||
// PseudoVersionBase returns the canonical parent version, if any, upon which
|
||||
// the pseudo-version v is based.
|
||||
//
|
||||
// If v has no parent version (that is, if it is "vX.0.0-[…]"),
|
||||
// PseudoVersionBase returns the empty string and a nil error.
|
||||
func PseudoVersionBase(v string) (string, error) {
|
||||
base, _, _, build, err := parsePseudoVersion(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
switch pre := semver.Prerelease(base); pre {
|
||||
case "":
|
||||
// vX.0.0-yyyymmddhhmmss-abcdef123456 → ""
|
||||
if build != "" {
|
||||
// Pseudo-versions of the form vX.0.0-yyyymmddhhmmss-abcdef123456+incompatible
|
||||
// are nonsensical: the "vX.0.0-" prefix implies that there is no parent tag,
|
||||
// but the "+incompatible" suffix implies that the major version of
|
||||
// the parent tag is not compatible with the module's import path.
|
||||
//
|
||||
// There are a few such entries in the index generated by proxy.golang.org,
|
||||
// but we believe those entries were generated by the proxy itself.
|
||||
return "", &InvalidVersionError{
|
||||
Version: v,
|
||||
Pseudo: true,
|
||||
Err: fmt.Errorf("lacks base version, but has build metadata %q", build),
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
|
||||
case "-0":
|
||||
// vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456 → vX.Y.Z
|
||||
// vX.Y.(Z+1)-0.yyyymmddhhmmss-abcdef123456+incompatible → vX.Y.Z+incompatible
|
||||
base = strings.TrimSuffix(base, pre)
|
||||
i := strings.LastIndexByte(base, '.')
|
||||
if i < 0 {
|
||||
panic("base from parsePseudoVersion missing patch number: " + base)
|
||||
}
|
||||
patch := decDecimal(base[i+1:])
|
||||
if patch == "" {
|
||||
// vX.0.0-0 is invalid, but has been observed in the wild in the index
|
||||
// generated by requests to proxy.golang.org.
|
||||
//
|
||||
// NOTE(bcmills): I cannot find a historical bug that accounts for
|
||||
// pseudo-versions of this form, nor have I seen such versions in any
|
||||
// actual go.mod files. If we find actual examples of this form and a
|
||||
// reasonable theory of how they came into existence, it seems fine to
|
||||
// treat them as equivalent to vX.0.0 (especially since the invalid
|
||||
// pseudo-versions have lower precedence than the real ones). For now, we
|
||||
// reject them.
|
||||
return "", &InvalidVersionError{
|
||||
Version: v,
|
||||
Pseudo: true,
|
||||
Err: fmt.Errorf("version before %s would have negative patch number", base),
|
||||
}
|
||||
}
|
||||
return base[:i+1] + patch + build, nil
|
||||
|
||||
default:
|
||||
// vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456 → vX.Y.Z-pre
|
||||
// vX.Y.Z-pre.0.yyyymmddhhmmss-abcdef123456+incompatible → vX.Y.Z-pre+incompatible
|
||||
if !strings.HasSuffix(base, ".0") {
|
||||
panic(`base from parsePseudoVersion missing ".0" before date: ` + base)
|
||||
}
|
||||
return strings.TrimSuffix(base, ".0") + build, nil
|
||||
}
|
||||
}
|
||||
|
||||
var errPseudoSyntax = errors.New("syntax error")
|
||||
|
||||
func parsePseudoVersion(v string) (base, timestamp, rev, build string, err error) {
|
||||
if !IsPseudoVersion(v) {
|
||||
return "", "", "", "", &InvalidVersionError{
|
||||
Version: v,
|
||||
Pseudo: true,
|
||||
Err: errPseudoSyntax,
|
||||
}
|
||||
}
|
||||
build = semver.Build(v)
|
||||
v = strings.TrimSuffix(v, build)
|
||||
j := strings.LastIndex(v, "-")
|
||||
v, rev = v[:j], v[j+1:]
|
||||
i := strings.LastIndex(v, "-")
|
||||
if j := strings.LastIndex(v, "."); j > i {
|
||||
base = v[:j] // "vX.Y.Z-pre.0" or "vX.Y.(Z+1)-0"
|
||||
timestamp = v[j+1:]
|
||||
} else {
|
||||
base = v[:i] // "vX.0.0"
|
||||
timestamp = v[i+1:]
|
||||
}
|
||||
return base, timestamp, rev, build, nil
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
// Copyright 2018 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 module
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
var pseudoTests = []struct {
|
||||
major string
|
||||
older string
|
||||
version string
|
||||
}{
|
||||
{"", "", "v0.0.0-20060102150405-hash"},
|
||||
{"v0", "", "v0.0.0-20060102150405-hash"},
|
||||
{"v1", "", "v1.0.0-20060102150405-hash"},
|
||||
{"v2", "", "v2.0.0-20060102150405-hash"},
|
||||
{"unused", "v0.0.0", "v0.0.1-0.20060102150405-hash"},
|
||||
{"unused", "v1.2.3", "v1.2.4-0.20060102150405-hash"},
|
||||
{"unused", "v1.2.99999999999999999", "v1.2.100000000000000000-0.20060102150405-hash"},
|
||||
{"unused", "v1.2.3-pre", "v1.2.3-pre.0.20060102150405-hash"},
|
||||
{"unused", "v1.3.0-pre", "v1.3.0-pre.0.20060102150405-hash"},
|
||||
{"unused", "v0.0.0--", "v0.0.0--.0.20060102150405-hash"},
|
||||
{"unused", "v1.0.0+metadata", "v1.0.1-0.20060102150405-hash+metadata"},
|
||||
{"unused", "v2.0.0+incompatible", "v2.0.1-0.20060102150405-hash+incompatible"},
|
||||
{"unused", "v2.3.0-pre+incompatible", "v2.3.0-pre.0.20060102150405-hash+incompatible"},
|
||||
}
|
||||
|
||||
var pseudoTime = time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)
|
||||
|
||||
func TestPseudoVersion(t *testing.T) {
|
||||
for _, tt := range pseudoTests {
|
||||
v := PseudoVersion(tt.major, tt.older, pseudoTime, "hash")
|
||||
if v != tt.version {
|
||||
t.Errorf("PseudoVersion(%q, %q, ...) = %v, want %v", tt.major, tt.older, v, tt.version)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsPseudoVersion(t *testing.T) {
|
||||
for _, tt := range pseudoTests {
|
||||
if !IsPseudoVersion(tt.version) {
|
||||
t.Errorf("IsPseudoVersion(%q) = false, want true", tt.version)
|
||||
}
|
||||
if IsPseudoVersion(tt.older) {
|
||||
t.Errorf("IsPseudoVersion(%q) = true, want false", tt.older)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPseudoVersionTime(t *testing.T) {
|
||||
for _, tt := range pseudoTests {
|
||||
tm, err := PseudoVersionTime(tt.version)
|
||||
if tm != pseudoTime || err != nil {
|
||||
t.Errorf("PseudoVersionTime(%q) = %v, %v, want %v, nil", tt.version, tm.Format(time.RFC3339), err, pseudoTime.Format(time.RFC3339))
|
||||
}
|
||||
tm, err = PseudoVersionTime(tt.older)
|
||||
if tm != (time.Time{}) || err == nil {
|
||||
t.Errorf("PseudoVersionTime(%q) = %v, %v, want %v, error", tt.older, tm.Format(time.RFC3339), err, time.Time{}.Format(time.RFC3339))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidPseudoVersionTime(t *testing.T) {
|
||||
const v = "---"
|
||||
if _, err := PseudoVersionTime(v); err == nil {
|
||||
t.Error("expected error, got nil instead")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPseudoVersionRev(t *testing.T) {
|
||||
for _, tt := range pseudoTests {
|
||||
rev, err := PseudoVersionRev(tt.version)
|
||||
if rev != "hash" || err != nil {
|
||||
t.Errorf("PseudoVersionRev(%q) = %q, %v, want %q, nil", tt.older, rev, err, "hash")
|
||||
}
|
||||
rev, err = PseudoVersionRev(tt.older)
|
||||
if rev != "" || err == nil {
|
||||
t.Errorf("PseudoVersionRev(%q) = %q, %v, want %q, error", tt.older, rev, err, "")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPseudoVersionBase(t *testing.T) {
|
||||
for _, tt := range pseudoTests {
|
||||
base, err := PseudoVersionBase(tt.version)
|
||||
if err != nil {
|
||||
t.Errorf("PseudoVersionBase(%q): %v", tt.version, err)
|
||||
} else if base != tt.older {
|
||||
t.Errorf("PseudoVersionBase(%q) = %q; want %q", tt.version, base, tt.older)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidPseudoVersionBase(t *testing.T) {
|
||||
for _, in := range []string{
|
||||
"v0.0.0",
|
||||
"v0.0.0-", // malformed: empty prerelease
|
||||
"v0.0.0-0.20060102150405-hash", // Z+1 == 0
|
||||
"v0.1.0-0.20060102150405-hash", // Z+1 == 0
|
||||
"v1.0.0-0.20060102150405-hash", // Z+1 == 0
|
||||
"v0.0.0-20060102150405-hash+incompatible", // "+incompatible without base version
|
||||
"v0.0.0-20060102150405-hash+metadata", // other metadata without base version
|
||||
} {
|
||||
base, err := PseudoVersionBase(in)
|
||||
if err == nil || base != "" {
|
||||
t.Errorf(`PseudoVersionBase(%q) = %q, %v; want "", error`, in, base, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncDecimal(t *testing.T) {
|
||||
cases := []struct {
|
||||
in, want string
|
||||
}{
|
||||
{"0", "1"},
|
||||
{"1", "2"},
|
||||
{"99", "100"},
|
||||
{"100", "101"},
|
||||
{"101", "102"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
got := incDecimal(tc.in)
|
||||
if got != tc.want {
|
||||
t.Fatalf("incDecimal(%q) = %q; want %q", tc.in, tc.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecDecimal(t *testing.T) {
|
||||
cases := []struct {
|
||||
in, want string
|
||||
}{
|
||||
{"", ""},
|
||||
{"0", ""},
|
||||
{"00", ""},
|
||||
{"1", "0"},
|
||||
{"2", "1"},
|
||||
{"99", "98"},
|
||||
{"100", "99"},
|
||||
{"101", "100"},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
got := decDecimal(tc.in)
|
||||
if got != tc.want {
|
||||
t.Fatalf("decDecimal(%q) = %q; want %q", tc.in, tc.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
// Copyright 2018 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 semver implements comparison of semantic version strings.
|
||||
// In this package, semantic version strings must begin with a leading "v",
|
||||
// as in "v1.0.0".
|
||||
//
|
||||
// The general form of a semantic version string accepted by this package is
|
||||
//
|
||||
// vMAJOR[.MINOR[.PATCH[-PRERELEASE][+BUILD]]]
|
||||
//
|
||||
// where square brackets indicate optional parts of the syntax;
|
||||
// MAJOR, MINOR, and PATCH are decimal integers without extra leading zeros;
|
||||
// PRERELEASE and BUILD are each a series of non-empty dot-separated identifiers
|
||||
// using only alphanumeric characters and hyphens; and
|
||||
// all-numeric PRERELEASE identifiers must not have leading zeros.
|
||||
//
|
||||
// This package follows Semantic Versioning 2.0.0 (see semver.org)
|
||||
// with two exceptions. First, it requires the "v" prefix. Second, it recognizes
|
||||
// vMAJOR and vMAJOR.MINOR (with no prerelease or build suffixes)
|
||||
// as shorthands for vMAJOR.0.0 and vMAJOR.MINOR.0.
|
||||
package semver
|
||||
|
||||
import "sort"
|
||||
|
||||
// parsed returns the parsed form of a semantic version string.
|
||||
type parsed struct {
|
||||
major string
|
||||
minor string
|
||||
patch string
|
||||
short string
|
||||
prerelease string
|
||||
build string
|
||||
}
|
||||
|
||||
// IsValid reports whether v is a valid semantic version string.
|
||||
func IsValid(v string) bool {
|
||||
_, ok := parse(v)
|
||||
return ok
|
||||
}
|
||||
|
||||
// Canonical returns the canonical formatting of the semantic version v.
|
||||
// It fills in any missing .MINOR or .PATCH and discards build metadata.
|
||||
// Two semantic versions compare equal only if their canonical formattings
|
||||
// are identical strings.
|
||||
// The canonical invalid semantic version is the empty string.
|
||||
func Canonical(v string) string {
|
||||
p, ok := parse(v)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
if p.build != "" {
|
||||
return v[:len(v)-len(p.build)]
|
||||
}
|
||||
if p.short != "" {
|
||||
return v + p.short
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
// Major returns the major version prefix of the semantic version v.
|
||||
// For example, Major("v2.1.0") == "v2".
|
||||
// If v is an invalid semantic version string, Major returns the empty string.
|
||||
func Major(v string) string {
|
||||
pv, ok := parse(v)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return v[:1+len(pv.major)]
|
||||
}
|
||||
|
||||
// MajorMinor returns the major.minor version prefix of the semantic version v.
|
||||
// For example, MajorMinor("v2.1.0") == "v2.1".
|
||||
// If v is an invalid semantic version string, MajorMinor returns the empty string.
|
||||
func MajorMinor(v string) string {
|
||||
pv, ok := parse(v)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
i := 1 + len(pv.major)
|
||||
if j := i + 1 + len(pv.minor); j <= len(v) && v[i] == '.' && v[i+1:j] == pv.minor {
|
||||
return v[:j]
|
||||
}
|
||||
return v[:i] + "." + pv.minor
|
||||
}
|
||||
|
||||
// Prerelease returns the prerelease suffix of the semantic version v.
|
||||
// For example, Prerelease("v2.1.0-pre+meta") == "-pre".
|
||||
// If v is an invalid semantic version string, Prerelease returns the empty string.
|
||||
func Prerelease(v string) string {
|
||||
pv, ok := parse(v)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return pv.prerelease
|
||||
}
|
||||
|
||||
// Build returns the build suffix of the semantic version v.
|
||||
// For example, Build("v2.1.0+meta") == "+meta".
|
||||
// If v is an invalid semantic version string, Build returns the empty string.
|
||||
func Build(v string) string {
|
||||
pv, ok := parse(v)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return pv.build
|
||||
}
|
||||
|
||||
// Compare returns an integer comparing two versions according to
|
||||
// semantic version precedence.
|
||||
// The result will be 0 if v == w, -1 if v < w, or +1 if v > w.
|
||||
//
|
||||
// An invalid semantic version string is considered less than a valid one.
|
||||
// All invalid semantic version strings compare equal to each other.
|
||||
func Compare(v, w string) int {
|
||||
pv, ok1 := parse(v)
|
||||
pw, ok2 := parse(w)
|
||||
if !ok1 && !ok2 {
|
||||
return 0
|
||||
}
|
||||
if !ok1 {
|
||||
return -1
|
||||
}
|
||||
if !ok2 {
|
||||
return +1
|
||||
}
|
||||
if c := compareInt(pv.major, pw.major); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := compareInt(pv.minor, pw.minor); c != 0 {
|
||||
return c
|
||||
}
|
||||
if c := compareInt(pv.patch, pw.patch); c != 0 {
|
||||
return c
|
||||
}
|
||||
return comparePrerelease(pv.prerelease, pw.prerelease)
|
||||
}
|
||||
|
||||
// Max canonicalizes its arguments and then returns the version string
|
||||
// that compares greater.
|
||||
//
|
||||
// Deprecated: use [Compare] instead. In most cases, returning a canonicalized
|
||||
// version is not expected or desired.
|
||||
func Max(v, w string) string {
|
||||
v = Canonical(v)
|
||||
w = Canonical(w)
|
||||
if Compare(v, w) > 0 {
|
||||
return v
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// ByVersion implements [sort.Interface] for sorting semantic version strings.
|
||||
type ByVersion []string
|
||||
|
||||
func (vs ByVersion) Len() int { return len(vs) }
|
||||
func (vs ByVersion) Swap(i, j int) { vs[i], vs[j] = vs[j], vs[i] }
|
||||
func (vs ByVersion) Less(i, j int) bool {
|
||||
cmp := Compare(vs[i], vs[j])
|
||||
if cmp != 0 {
|
||||
return cmp < 0
|
||||
}
|
||||
return vs[i] < vs[j]
|
||||
}
|
||||
|
||||
// Sort sorts a list of semantic version strings using [ByVersion].
|
||||
func Sort(list []string) {
|
||||
sort.Sort(ByVersion(list))
|
||||
}
|
||||
|
||||
func parse(v string) (p parsed, ok bool) {
|
||||
if v == "" || v[0] != 'v' {
|
||||
return
|
||||
}
|
||||
p.major, v, ok = parseInt(v[1:])
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if v == "" {
|
||||
p.minor = "0"
|
||||
p.patch = "0"
|
||||
p.short = ".0.0"
|
||||
return
|
||||
}
|
||||
if v[0] != '.' {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
p.minor, v, ok = parseInt(v[1:])
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if v == "" {
|
||||
p.patch = "0"
|
||||
p.short = ".0"
|
||||
return
|
||||
}
|
||||
if v[0] != '.' {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
p.patch, v, ok = parseInt(v[1:])
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if len(v) > 0 && v[0] == '-' {
|
||||
p.prerelease, v, ok = parsePrerelease(v)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
if len(v) > 0 && v[0] == '+' {
|
||||
p.build, v, ok = parseBuild(v)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
if v != "" {
|
||||
ok = false
|
||||
return
|
||||
}
|
||||
ok = true
|
||||
return
|
||||
}
|
||||
|
||||
func parseInt(v string) (t, rest string, ok bool) {
|
||||
if v == "" {
|
||||
return
|
||||
}
|
||||
if v[0] < '0' || '9' < v[0] {
|
||||
return
|
||||
}
|
||||
i := 1
|
||||
for i < len(v) && '0' <= v[i] && v[i] <= '9' {
|
||||
i++
|
||||
}
|
||||
if v[0] == '0' && i != 1 {
|
||||
return
|
||||
}
|
||||
return v[:i], v[i:], true
|
||||
}
|
||||
|
||||
func parsePrerelease(v string) (t, rest string, ok bool) {
|
||||
// "A pre-release version MAY be denoted by appending a hyphen and
|
||||
// a series of dot separated identifiers immediately following the patch version.
|
||||
// Identifiers MUST comprise only ASCII alphanumerics and hyphen [0-9A-Za-z-].
|
||||
// Identifiers MUST NOT be empty. Numeric identifiers MUST NOT include leading zeroes."
|
||||
if v == "" || v[0] != '-' {
|
||||
return
|
||||
}
|
||||
i := 1
|
||||
start := 1
|
||||
for i < len(v) && v[i] != '+' {
|
||||
if !isIdentChar(v[i]) && v[i] != '.' {
|
||||
return
|
||||
}
|
||||
if v[i] == '.' {
|
||||
if start == i || isBadNum(v[start:i]) {
|
||||
return
|
||||
}
|
||||
start = i + 1
|
||||
}
|
||||
i++
|
||||
}
|
||||
if start == i || isBadNum(v[start:i]) {
|
||||
return
|
||||
}
|
||||
return v[:i], v[i:], true
|
||||
}
|
||||
|
||||
func parseBuild(v string) (t, rest string, ok bool) {
|
||||
if v == "" || v[0] != '+' {
|
||||
return
|
||||
}
|
||||
i := 1
|
||||
start := 1
|
||||
for i < len(v) {
|
||||
if !isIdentChar(v[i]) && v[i] != '.' {
|
||||
return
|
||||
}
|
||||
if v[i] == '.' {
|
||||
if start == i {
|
||||
return
|
||||
}
|
||||
start = i + 1
|
||||
}
|
||||
i++
|
||||
}
|
||||
if start == i {
|
||||
return
|
||||
}
|
||||
return v[:i], v[i:], true
|
||||
}
|
||||
|
||||
func isIdentChar(c byte) bool {
|
||||
return 'A' <= c && c <= 'Z' || 'a' <= c && c <= 'z' || '0' <= c && c <= '9' || c == '-'
|
||||
}
|
||||
|
||||
func isBadNum(v string) bool {
|
||||
i := 0
|
||||
for i < len(v) && '0' <= v[i] && v[i] <= '9' {
|
||||
i++
|
||||
}
|
||||
return i == len(v) && i > 1 && v[0] == '0'
|
||||
}
|
||||
|
||||
func isNum(v string) bool {
|
||||
i := 0
|
||||
for i < len(v) && '0' <= v[i] && v[i] <= '9' {
|
||||
i++
|
||||
}
|
||||
return i == len(v)
|
||||
}
|
||||
|
||||
func compareInt(x, y string) int {
|
||||
if x == y {
|
||||
return 0
|
||||
}
|
||||
if len(x) < len(y) {
|
||||
return -1
|
||||
}
|
||||
if len(x) > len(y) {
|
||||
return +1
|
||||
}
|
||||
if x < y {
|
||||
return -1
|
||||
} else {
|
||||
return +1
|
||||
}
|
||||
}
|
||||
|
||||
func comparePrerelease(x, y string) int {
|
||||
// "When major, minor, and patch are equal, a pre-release version has
|
||||
// lower precedence than a normal version.
|
||||
// Example: 1.0.0-alpha < 1.0.0.
|
||||
// Precedence for two pre-release versions with the same major, minor,
|
||||
// and patch version MUST be determined by comparing each dot separated
|
||||
// identifier from left to right until a difference is found as follows:
|
||||
// identifiers consisting of only digits are compared numerically and
|
||||
// identifiers with letters or hyphens are compared lexically in ASCII
|
||||
// sort order. Numeric identifiers always have lower precedence than
|
||||
// non-numeric identifiers. A larger set of pre-release fields has a
|
||||
// higher precedence than a smaller set, if all of the preceding
|
||||
// identifiers are equal.
|
||||
// Example: 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta <
|
||||
// 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0."
|
||||
if x == y {
|
||||
return 0
|
||||
}
|
||||
if x == "" {
|
||||
return +1
|
||||
}
|
||||
if y == "" {
|
||||
return -1
|
||||
}
|
||||
for x != "" && y != "" {
|
||||
x = x[1:] // skip - or .
|
||||
y = y[1:] // skip - or .
|
||||
var dx, dy string
|
||||
dx, x = nextIdent(x)
|
||||
dy, y = nextIdent(y)
|
||||
if dx != dy {
|
||||
ix := isNum(dx)
|
||||
iy := isNum(dy)
|
||||
if ix != iy {
|
||||
if ix {
|
||||
return -1
|
||||
} else {
|
||||
return +1
|
||||
}
|
||||
}
|
||||
if ix {
|
||||
if len(dx) < len(dy) {
|
||||
return -1
|
||||
}
|
||||
if len(dx) > len(dy) {
|
||||
return +1
|
||||
}
|
||||
}
|
||||
if dx < dy {
|
||||
return -1
|
||||
} else {
|
||||
return +1
|
||||
}
|
||||
}
|
||||
}
|
||||
if x == "" {
|
||||
return -1
|
||||
} else {
|
||||
return +1
|
||||
}
|
||||
}
|
||||
|
||||
func nextIdent(x string) (dx, rest string) {
|
||||
i := 0
|
||||
for i < len(x) && x[i] != '.' {
|
||||
i++
|
||||
}
|
||||
return x[:i], x[i:]
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
// Copyright 2018 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 semver
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var tests = []struct {
|
||||
in string
|
||||
out string
|
||||
}{
|
||||
{"bad", ""},
|
||||
{"v1-alpha.beta.gamma", ""},
|
||||
{"v1-pre", ""},
|
||||
{"v1+meta", ""},
|
||||
{"v1-pre+meta", ""},
|
||||
{"v1.2-pre", ""},
|
||||
{"v1.2+meta", ""},
|
||||
{"v1.2-pre+meta", ""},
|
||||
{"v1.0.0-alpha", "v1.0.0-alpha"},
|
||||
{"v1.0.0-alpha.1", "v1.0.0-alpha.1"},
|
||||
{"v1.0.0-alpha.beta", "v1.0.0-alpha.beta"},
|
||||
{"v1.0.0-beta", "v1.0.0-beta"},
|
||||
{"v1.0.0-beta.2", "v1.0.0-beta.2"},
|
||||
{"v1.0.0-beta.11", "v1.0.0-beta.11"},
|
||||
{"v1.0.0-rc.1", "v1.0.0-rc.1"},
|
||||
{"v1", "v1.0.0"},
|
||||
{"v1.0", "v1.0.0"},
|
||||
{"v1.0.0", "v1.0.0"},
|
||||
{"v1.2", "v1.2.0"},
|
||||
{"v1.2.0", "v1.2.0"},
|
||||
{"v1.2.3-456", "v1.2.3-456"},
|
||||
{"v1.2.3-456.789", "v1.2.3-456.789"},
|
||||
{"v1.2.3-456-789", "v1.2.3-456-789"},
|
||||
{"v1.2.3-456a", "v1.2.3-456a"},
|
||||
{"v1.2.3-pre", "v1.2.3-pre"},
|
||||
{"v1.2.3-pre+meta", "v1.2.3-pre"},
|
||||
{"v1.2.3-pre.1", "v1.2.3-pre.1"},
|
||||
{"v1.2.3-zzz", "v1.2.3-zzz"},
|
||||
{"v1.2.3", "v1.2.3"},
|
||||
{"v1.2.3+meta", "v1.2.3"},
|
||||
{"v1.2.3+meta-pre", "v1.2.3"},
|
||||
{"v1.2.3+meta-pre.sha.256a", "v1.2.3"},
|
||||
}
|
||||
|
||||
func TestIsValid(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
ok := IsValid(tt.in)
|
||||
if ok != (tt.out != "") {
|
||||
t.Errorf("IsValid(%q) = %v, want %v", tt.in, ok, !ok)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanonical(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
out := Canonical(tt.in)
|
||||
if out != tt.out {
|
||||
t.Errorf("Canonical(%q) = %q, want %q", tt.in, out, tt.out)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMajor(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
out := Major(tt.in)
|
||||
want := ""
|
||||
if i := strings.Index(tt.out, "."); i >= 0 {
|
||||
want = tt.out[:i]
|
||||
}
|
||||
if out != want {
|
||||
t.Errorf("Major(%q) = %q, want %q", tt.in, out, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMajorMinor(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
out := MajorMinor(tt.in)
|
||||
var want string
|
||||
if tt.out != "" {
|
||||
want = tt.in
|
||||
if i := strings.Index(want, "+"); i >= 0 {
|
||||
want = want[:i]
|
||||
}
|
||||
if i := strings.Index(want, "-"); i >= 0 {
|
||||
want = want[:i]
|
||||
}
|
||||
switch strings.Count(want, ".") {
|
||||
case 0:
|
||||
want += ".0"
|
||||
case 1:
|
||||
// ok
|
||||
case 2:
|
||||
want = want[:strings.LastIndex(want, ".")]
|
||||
}
|
||||
}
|
||||
if out != want {
|
||||
t.Errorf("MajorMinor(%q) = %q, want %q", tt.in, out, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrerelease(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
pre := Prerelease(tt.in)
|
||||
var want string
|
||||
if tt.out != "" {
|
||||
if i := strings.Index(tt.out, "-"); i >= 0 {
|
||||
want = tt.out[i:]
|
||||
}
|
||||
}
|
||||
if pre != want {
|
||||
t.Errorf("Prerelease(%q) = %q, want %q", tt.in, pre, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuild(t *testing.T) {
|
||||
for _, tt := range tests {
|
||||
build := Build(tt.in)
|
||||
var want string
|
||||
if tt.out != "" {
|
||||
if i := strings.Index(tt.in, "+"); i >= 0 {
|
||||
want = tt.in[i:]
|
||||
}
|
||||
}
|
||||
if build != want {
|
||||
t.Errorf("Build(%q) = %q, want %q", tt.in, build, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompare(t *testing.T) {
|
||||
for i, ti := range tests {
|
||||
for j, tj := range tests {
|
||||
cmp := Compare(ti.in, tj.in)
|
||||
var want int
|
||||
if ti.out == tj.out {
|
||||
want = 0
|
||||
} else if i < j {
|
||||
want = -1
|
||||
} else {
|
||||
want = +1
|
||||
}
|
||||
if cmp != want {
|
||||
t.Errorf("Compare(%q, %q) = %d, want %d", ti.in, tj.in, cmp, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSort(t *testing.T) {
|
||||
versions := make([]string, len(tests))
|
||||
for i, test := range tests {
|
||||
versions[i] = test.in
|
||||
}
|
||||
rand.Shuffle(len(versions), func(i, j int) { versions[i], versions[j] = versions[j], versions[i] })
|
||||
Sort(versions)
|
||||
if !sort.IsSorted(ByVersion(versions)) {
|
||||
t.Errorf("list is not sorted:\n%s", strings.Join(versions, "\n"))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMax(t *testing.T) {
|
||||
for i, ti := range tests {
|
||||
for j, tj := range tests {
|
||||
max := Max(ti.in, tj.in)
|
||||
want := Canonical(ti.in)
|
||||
if i < j {
|
||||
want = Canonical(tj.in)
|
||||
}
|
||||
if max != want {
|
||||
t.Errorf("Max(%q, %q) = %q, want %q", ti.in, tj.in, max, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
v1 = "v1.0.0+metadata-dash"
|
||||
v2 = "v1.0.0+metadata-dash1"
|
||||
)
|
||||
|
||||
func BenchmarkCompare(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
if Compare(v1, v2) != 0 {
|
||||
b.Fatalf("bad compare")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
// Copyright 2018 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.
|
||||
|
||||
// Parallel cache.
|
||||
// This file is copied from cmd/go/internal/par.
|
||||
|
||||
package sumdb
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// parCache runs an action once per key and caches the result.
|
||||
type parCache struct {
|
||||
m sync.Map
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
done uint32
|
||||
mu sync.Mutex
|
||||
result interface{}
|
||||
}
|
||||
|
||||
// Do calls the function f if and only if Do is being called for the first time with this key.
|
||||
// No call to Do with a given key returns until the one call to f returns.
|
||||
// Do returns the value returned by the one call to f.
|
||||
func (c *parCache) Do(key interface{}, f func() interface{}) interface{} {
|
||||
entryIface, ok := c.m.Load(key)
|
||||
if !ok {
|
||||
entryIface, _ = c.m.LoadOrStore(key, new(cacheEntry))
|
||||
}
|
||||
e := entryIface.(*cacheEntry)
|
||||
if atomic.LoadUint32(&e.done) == 0 {
|
||||
e.mu.Lock()
|
||||
if atomic.LoadUint32(&e.done) == 0 {
|
||||
e.result = f()
|
||||
atomic.StoreUint32(&e.done, 1)
|
||||
}
|
||||
e.mu.Unlock()
|
||||
}
|
||||
return e.result
|
||||
}
|
||||
|
||||
// Get returns the cached result associated with key.
|
||||
// It returns nil if there is no such result.
|
||||
// If the result for key is being computed, Get does not wait for the computation to finish.
|
||||
func (c *parCache) Get(key interface{}) interface{} {
|
||||
entryIface, ok := c.m.Load(key)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
e := entryIface.(*cacheEntry)
|
||||
if atomic.LoadUint32(&e.done) == 0 {
|
||||
return nil
|
||||
}
|
||||
return e.result
|
||||
}
|
||||
@@ -0,0 +1,676 @@
|
||||
// Copyright 2019 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 sumdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
|
||||
"golang.org/x/mod/module"
|
||||
"golang.org/x/mod/sumdb/note"
|
||||
"golang.org/x/mod/sumdb/tlog"
|
||||
)
|
||||
|
||||
// A ClientOps provides the external operations
|
||||
// (file caching, HTTP fetches, and so on) needed by the [Client].
|
||||
// The methods must be safe for concurrent use by multiple goroutines.
|
||||
type ClientOps interface {
|
||||
// ReadRemote reads and returns the content served at the given path
|
||||
// on the remote database server. The path begins with "/lookup" or "/tile/",
|
||||
// and there is no need to parse the path in any way.
|
||||
// It is the implementation's responsibility to turn that path into a full URL
|
||||
// and make the HTTP request. ReadRemote should return an error for
|
||||
// any non-200 HTTP response status.
|
||||
ReadRemote(path string) ([]byte, error)
|
||||
|
||||
// ReadConfig reads and returns the content of the named configuration file.
|
||||
// There are only a fixed set of configuration files.
|
||||
//
|
||||
// "key" returns a file containing the verifier key for the server.
|
||||
//
|
||||
// serverName + "/latest" returns a file containing the latest known
|
||||
// signed tree from the server.
|
||||
// To signal that the client wishes to start with an "empty" signed tree,
|
||||
// ReadConfig can return a successful empty result (0 bytes of data).
|
||||
ReadConfig(file string) ([]byte, error)
|
||||
|
||||
// WriteConfig updates the content of the named configuration file,
|
||||
// changing it from the old []byte to the new []byte.
|
||||
// If the old []byte does not match the stored configuration,
|
||||
// WriteConfig must return ErrWriteConflict.
|
||||
// Otherwise, WriteConfig should atomically replace old with new.
|
||||
// The "key" configuration file is never written using WriteConfig.
|
||||
WriteConfig(file string, old, new []byte) error
|
||||
|
||||
// ReadCache reads and returns the content of the named cache file.
|
||||
// Any returned error will be treated as equivalent to the file not existing.
|
||||
// There can be arbitrarily many cache files, such as:
|
||||
// serverName/lookup/pkg@version
|
||||
// serverName/tile/8/1/x123/456
|
||||
ReadCache(file string) ([]byte, error)
|
||||
|
||||
// WriteCache writes the named cache file.
|
||||
WriteCache(file string, data []byte)
|
||||
|
||||
// Log prints the given log message (such as with log.Print)
|
||||
Log(msg string)
|
||||
|
||||
// SecurityError prints the given security error log message.
|
||||
// The Client returns ErrSecurity from any operation that invokes SecurityError,
|
||||
// but the return value is mainly for testing. In a real program,
|
||||
// SecurityError should typically print the message and call log.Fatal or os.Exit.
|
||||
SecurityError(msg string)
|
||||
}
|
||||
|
||||
// ErrWriteConflict signals a write conflict during Client.WriteConfig.
|
||||
var ErrWriteConflict = errors.New("write conflict")
|
||||
|
||||
// ErrSecurity is returned by [Client] operations that invoke Client.SecurityError.
|
||||
var ErrSecurity = errors.New("security error: misbehaving server")
|
||||
|
||||
// A Client is a client connection to a checksum database.
|
||||
// All the methods are safe for simultaneous use by multiple goroutines.
|
||||
type Client struct {
|
||||
ops ClientOps // access to operations in the external world
|
||||
|
||||
didLookup uint32
|
||||
|
||||
// one-time initialized data
|
||||
initOnce sync.Once
|
||||
initErr error // init error, if any
|
||||
name string // name of accepted verifier
|
||||
verifiers note.Verifiers // accepted verifiers (just one, but Verifiers for note.Open)
|
||||
tileReader tileReader
|
||||
tileHeight int
|
||||
nosumdb string
|
||||
|
||||
record parCache // cache of record lookup, keyed by path@vers
|
||||
tileCache parCache // cache of c.readTile, keyed by tile
|
||||
|
||||
latestMu sync.Mutex
|
||||
latest tlog.Tree // latest known tree head
|
||||
latestMsg []byte // encoded signed note for latest
|
||||
|
||||
tileSavedMu sync.Mutex
|
||||
tileSaved map[tlog.Tile]bool // which tiles have been saved using c.ops.WriteCache already
|
||||
}
|
||||
|
||||
// NewClient returns a new [Client] using the given [ClientOps].
|
||||
func NewClient(ops ClientOps) *Client {
|
||||
return &Client{
|
||||
ops: ops,
|
||||
}
|
||||
}
|
||||
|
||||
// init initializes the client (if not already initialized)
|
||||
// and returns any initialization error.
|
||||
func (c *Client) init() error {
|
||||
c.initOnce.Do(c.initWork)
|
||||
return c.initErr
|
||||
}
|
||||
|
||||
// initWork does the actual initialization work.
|
||||
func (c *Client) initWork() {
|
||||
defer func() {
|
||||
if c.initErr != nil {
|
||||
c.initErr = fmt.Errorf("initializing sumdb.Client: %v", c.initErr)
|
||||
}
|
||||
}()
|
||||
|
||||
c.tileReader.c = c
|
||||
if c.tileHeight == 0 {
|
||||
c.tileHeight = 8
|
||||
}
|
||||
c.tileSaved = make(map[tlog.Tile]bool)
|
||||
|
||||
vkey, err := c.ops.ReadConfig("key")
|
||||
if err != nil {
|
||||
c.initErr = err
|
||||
return
|
||||
}
|
||||
verifier, err := note.NewVerifier(strings.TrimSpace(string(vkey)))
|
||||
if err != nil {
|
||||
c.initErr = err
|
||||
return
|
||||
}
|
||||
c.verifiers = note.VerifierList(verifier)
|
||||
c.name = verifier.Name()
|
||||
|
||||
data, err := c.ops.ReadConfig(c.name + "/latest")
|
||||
if err != nil {
|
||||
c.initErr = err
|
||||
return
|
||||
}
|
||||
if err := c.mergeLatest(data); err != nil {
|
||||
c.initErr = err
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// SetTileHeight sets the tile height for the Client.
|
||||
// Any call to SetTileHeight must happen before the first call to [Client.Lookup].
|
||||
// If SetTileHeight is not called, the Client defaults to tile height 8.
|
||||
// SetTileHeight can be called at most once,
|
||||
// and if so it must be called before the first call to Lookup.
|
||||
func (c *Client) SetTileHeight(height int) {
|
||||
if atomic.LoadUint32(&c.didLookup) != 0 {
|
||||
panic("SetTileHeight used after Lookup")
|
||||
}
|
||||
if height <= 0 {
|
||||
panic("invalid call to SetTileHeight")
|
||||
}
|
||||
if c.tileHeight != 0 {
|
||||
panic("multiple calls to SetTileHeight")
|
||||
}
|
||||
c.tileHeight = height
|
||||
}
|
||||
|
||||
// SetGONOSUMDB sets the list of comma-separated GONOSUMDB patterns for the Client.
|
||||
// For any module path matching one of the patterns,
|
||||
// [Client.Lookup] will return ErrGONOSUMDB.
|
||||
// SetGONOSUMDB can be called at most once,
|
||||
// and if so it must be called before the first call to Lookup.
|
||||
func (c *Client) SetGONOSUMDB(list string) {
|
||||
if atomic.LoadUint32(&c.didLookup) != 0 {
|
||||
panic("SetGONOSUMDB used after Lookup")
|
||||
}
|
||||
if c.nosumdb != "" {
|
||||
panic("multiple calls to SetGONOSUMDB")
|
||||
}
|
||||
c.nosumdb = list
|
||||
}
|
||||
|
||||
// ErrGONOSUMDB is returned by [Client.Lookup] for paths that match
|
||||
// a pattern listed in the GONOSUMDB list (set by [Client.SetGONOSUMDB],
|
||||
// usually from the environment variable).
|
||||
var ErrGONOSUMDB = errors.New("skipped (listed in GONOSUMDB)")
|
||||
|
||||
func (c *Client) skip(target string) bool {
|
||||
return globsMatchPath(c.nosumdb, target)
|
||||
}
|
||||
|
||||
// globsMatchPath reports whether any path prefix of target
|
||||
// matches one of the glob patterns (as defined by path.Match)
|
||||
// in the comma-separated globs list.
|
||||
// It ignores any empty or malformed patterns in the list.
|
||||
func globsMatchPath(globs, target string) bool {
|
||||
for globs != "" {
|
||||
// Extract next non-empty glob in comma-separated list.
|
||||
var glob string
|
||||
if i := strings.Index(globs, ","); i >= 0 {
|
||||
glob, globs = globs[:i], globs[i+1:]
|
||||
} else {
|
||||
glob, globs = globs, ""
|
||||
}
|
||||
if glob == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// A glob with N+1 path elements (N slashes) needs to be matched
|
||||
// against the first N+1 path elements of target,
|
||||
// which end just before the N+1'th slash.
|
||||
n := strings.Count(glob, "/")
|
||||
prefix := target
|
||||
// Walk target, counting slashes, truncating at the N+1'th slash.
|
||||
for i := 0; i < len(target); i++ {
|
||||
if target[i] == '/' {
|
||||
if n == 0 {
|
||||
prefix = target[:i]
|
||||
break
|
||||
}
|
||||
n--
|
||||
}
|
||||
}
|
||||
if n > 0 {
|
||||
// Not enough prefix elements.
|
||||
continue
|
||||
}
|
||||
matched, _ := path.Match(glob, prefix)
|
||||
if matched {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Lookup returns the go.sum lines for the given module path and version.
|
||||
// The version may end in a /go.mod suffix, in which case Lookup returns
|
||||
// the go.sum lines for the module's go.mod-only hash.
|
||||
func (c *Client) Lookup(path, vers string) (lines []string, err error) {
|
||||
atomic.StoreUint32(&c.didLookup, 1)
|
||||
|
||||
if c.skip(path) {
|
||||
return nil, ErrGONOSUMDB
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
err = fmt.Errorf("%s@%s: %v", path, vers, err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := c.init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Prepare encoded cache filename / URL.
|
||||
epath, err := module.EscapePath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
evers, err := module.EscapeVersion(strings.TrimSuffix(vers, "/go.mod"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
remotePath := "/lookup/" + epath + "@" + evers
|
||||
file := c.name + remotePath
|
||||
|
||||
// Fetch the data.
|
||||
// The lookupCache avoids redundant ReadCache/GetURL operations
|
||||
// (especially since go.sum lines tend to come in pairs for a given
|
||||
// path and version) and also avoids having multiple of the same
|
||||
// request in flight at once.
|
||||
type cached struct {
|
||||
data []byte
|
||||
err error
|
||||
}
|
||||
result := c.record.Do(file, func() interface{} {
|
||||
// Try the on-disk cache, or else get from web.
|
||||
writeCache := false
|
||||
data, err := c.ops.ReadCache(file)
|
||||
if err != nil {
|
||||
data, err = c.ops.ReadRemote(remotePath)
|
||||
if err != nil {
|
||||
return cached{nil, err}
|
||||
}
|
||||
writeCache = true
|
||||
}
|
||||
|
||||
// Validate the record before using it for anything.
|
||||
id, text, treeMsg, err := tlog.ParseRecord(data)
|
||||
if err != nil {
|
||||
return cached{nil, err}
|
||||
}
|
||||
if err := c.mergeLatest(treeMsg); err != nil {
|
||||
return cached{nil, err}
|
||||
}
|
||||
if err := c.checkRecord(id, text); err != nil {
|
||||
return cached{nil, err}
|
||||
}
|
||||
|
||||
// Now that we've validated the record,
|
||||
// save it to the on-disk cache (unless that's where it came from).
|
||||
if writeCache {
|
||||
c.ops.WriteCache(file, data)
|
||||
}
|
||||
|
||||
return cached{data, nil}
|
||||
}).(cached)
|
||||
if result.err != nil {
|
||||
return nil, result.err
|
||||
}
|
||||
|
||||
// Extract the lines for the specific version we want
|
||||
// (with or without /go.mod).
|
||||
prefix := path + " " + vers + " "
|
||||
var hashes []string
|
||||
for _, line := range strings.Split(string(result.data), "\n") {
|
||||
if strings.HasPrefix(line, prefix) {
|
||||
hashes = append(hashes, line)
|
||||
}
|
||||
}
|
||||
return hashes, nil
|
||||
}
|
||||
|
||||
// mergeLatest merges the tree head in msg
|
||||
// with the Client's current latest tree head,
|
||||
// ensuring the result is a consistent timeline.
|
||||
// If the result is inconsistent, mergeLatest calls c.ops.SecurityError
|
||||
// with a detailed security error message and then
|
||||
// (only if c.ops.SecurityError does not exit the program) returns ErrSecurity.
|
||||
// If the Client's current latest tree head moves forward,
|
||||
// mergeLatest updates the underlying configuration file as well,
|
||||
// taking care to merge any independent updates to that configuration.
|
||||
func (c *Client) mergeLatest(msg []byte) error {
|
||||
// Merge msg into our in-memory copy of the latest tree head.
|
||||
when, err := c.mergeLatestMem(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if when != msgFuture {
|
||||
// msg matched our present or was in the past.
|
||||
// No change to our present, so no update of config file.
|
||||
return nil
|
||||
}
|
||||
|
||||
// Flush our extended timeline back out to the configuration file.
|
||||
// If the configuration file has been updated in the interim,
|
||||
// we need to merge any updates made there as well.
|
||||
// Note that writeConfig is an atomic compare-and-swap.
|
||||
for {
|
||||
msg, err := c.ops.ReadConfig(c.name + "/latest")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
when, err := c.mergeLatestMem(msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if when != msgPast {
|
||||
// msg matched our present or was from the future,
|
||||
// and now our in-memory copy matches.
|
||||
return nil
|
||||
}
|
||||
|
||||
// msg (== config) is in the past, so we need to update it.
|
||||
c.latestMu.Lock()
|
||||
latestMsg := c.latestMsg
|
||||
c.latestMu.Unlock()
|
||||
if err := c.ops.WriteConfig(c.name+"/latest", msg, latestMsg); err != ErrWriteConflict {
|
||||
// Success or a non-write-conflict error.
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
msgPast = 1 + iota
|
||||
msgNow
|
||||
msgFuture
|
||||
)
|
||||
|
||||
// mergeLatestMem is like mergeLatest but is only concerned with
|
||||
// updating the in-memory copy of the latest tree head (c.latest)
|
||||
// not the configuration file.
|
||||
// The when result explains when msg happened relative to our
|
||||
// previous idea of c.latest:
|
||||
// msgPast means msg was from before c.latest,
|
||||
// msgNow means msg was exactly c.latest, and
|
||||
// msgFuture means msg was from after c.latest, which has now been updated.
|
||||
func (c *Client) mergeLatestMem(msg []byte) (when int, err error) {
|
||||
if len(msg) == 0 {
|
||||
// Accept empty msg as the unsigned, empty timeline.
|
||||
c.latestMu.Lock()
|
||||
latest := c.latest
|
||||
c.latestMu.Unlock()
|
||||
if latest.N == 0 {
|
||||
return msgNow, nil
|
||||
}
|
||||
return msgPast, nil
|
||||
}
|
||||
|
||||
note, err := note.Open(msg, c.verifiers)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("reading tree note: %v\nnote:\n%s", err, msg)
|
||||
}
|
||||
tree, err := tlog.ParseTree([]byte(note.Text))
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("reading tree: %v\ntree:\n%s", err, note.Text)
|
||||
}
|
||||
|
||||
// Other lookups may be calling mergeLatest with other heads,
|
||||
// so c.latest is changing underfoot. We don't want to hold the
|
||||
// c.mu lock during tile fetches, so loop trying to update c.latest.
|
||||
c.latestMu.Lock()
|
||||
latest := c.latest
|
||||
latestMsg := c.latestMsg
|
||||
c.latestMu.Unlock()
|
||||
|
||||
for {
|
||||
// If the tree head looks old, check that it is on our timeline.
|
||||
if tree.N <= latest.N {
|
||||
if err := c.checkTrees(tree, msg, latest, latestMsg); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if tree.N < latest.N {
|
||||
return msgPast, nil
|
||||
}
|
||||
return msgNow, nil
|
||||
}
|
||||
|
||||
// The tree head looks new. Check that we are on its timeline and try to move our timeline forward.
|
||||
if err := c.checkTrees(latest, latestMsg, tree, msg); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
// Install our msg if possible.
|
||||
// Otherwise we will go around again.
|
||||
c.latestMu.Lock()
|
||||
installed := false
|
||||
if c.latest == latest {
|
||||
installed = true
|
||||
c.latest = tree
|
||||
c.latestMsg = msg
|
||||
} else {
|
||||
latest = c.latest
|
||||
latestMsg = c.latestMsg
|
||||
}
|
||||
c.latestMu.Unlock()
|
||||
|
||||
if installed {
|
||||
return msgFuture, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// checkTrees checks that older (from olderNote) is contained in newer (from newerNote).
|
||||
// If an error occurs, such as malformed data or a network problem, checkTrees returns that error.
|
||||
// If on the other hand checkTrees finds evidence of misbehavior, it prepares a detailed
|
||||
// message and calls log.Fatal.
|
||||
func (c *Client) checkTrees(older tlog.Tree, olderNote []byte, newer tlog.Tree, newerNote []byte) error {
|
||||
thr := tlog.TileHashReader(newer, &c.tileReader)
|
||||
h, err := tlog.TreeHash(older.N, thr)
|
||||
if err != nil {
|
||||
if older.N == newer.N {
|
||||
return fmt.Errorf("checking tree#%d: %v", older.N, err)
|
||||
}
|
||||
return fmt.Errorf("checking tree#%d against tree#%d: %v", older.N, newer.N, err)
|
||||
}
|
||||
if h == older.Hash {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Detected a fork in the tree timeline.
|
||||
// Start by reporting the inconsistent signed tree notes.
|
||||
var buf bytes.Buffer
|
||||
fmt.Fprintf(&buf, "SECURITY ERROR\n")
|
||||
fmt.Fprintf(&buf, "go.sum database server misbehavior detected!\n\n")
|
||||
indent := func(b []byte) []byte {
|
||||
return bytes.Replace(b, []byte("\n"), []byte("\n\t"), -1)
|
||||
}
|
||||
fmt.Fprintf(&buf, "old database:\n\t%s\n", indent(olderNote))
|
||||
fmt.Fprintf(&buf, "new database:\n\t%s\n", indent(newerNote))
|
||||
|
||||
// The notes alone are not enough to prove the inconsistency.
|
||||
// We also need to show that the newer note's tree hash for older.N
|
||||
// does not match older.Hash. The consumer of this report could
|
||||
// of course consult the server to try to verify the inconsistency,
|
||||
// but we are holding all the bits we need to prove it right now,
|
||||
// so we might as well print them and make the report not depend
|
||||
// on the continued availability of the misbehaving server.
|
||||
// Preparing this data only reuses the tiled hashes needed for
|
||||
// tlog.TreeHash(older.N, thr) above, so assuming thr is caching tiles,
|
||||
// there are no new access to the server here, and these operations cannot fail.
|
||||
fmt.Fprintf(&buf, "proof of misbehavior:\n\t%v", h)
|
||||
if p, err := tlog.ProveTree(newer.N, older.N, thr); err != nil {
|
||||
fmt.Fprintf(&buf, "\tinternal error: %v\n", err)
|
||||
} else if err := tlog.CheckTree(p, newer.N, newer.Hash, older.N, h); err != nil {
|
||||
fmt.Fprintf(&buf, "\tinternal error: generated inconsistent proof\n")
|
||||
} else {
|
||||
for _, h := range p {
|
||||
fmt.Fprintf(&buf, "\n\t%v", h)
|
||||
}
|
||||
}
|
||||
c.ops.SecurityError(buf.String())
|
||||
return ErrSecurity
|
||||
}
|
||||
|
||||
// checkRecord checks that record #id's hash matches data.
|
||||
func (c *Client) checkRecord(id int64, data []byte) error {
|
||||
c.latestMu.Lock()
|
||||
latest := c.latest
|
||||
c.latestMu.Unlock()
|
||||
|
||||
if id >= latest.N {
|
||||
return fmt.Errorf("cannot validate record %d in tree of size %d", id, latest.N)
|
||||
}
|
||||
hashes, err := tlog.TileHashReader(latest, &c.tileReader).ReadHashes([]int64{tlog.StoredHashIndex(0, id)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if hashes[0] == tlog.RecordHash(data) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("cannot authenticate record data in server response")
|
||||
}
|
||||
|
||||
// tileReader is a *Client wrapper that implements tlog.TileReader.
|
||||
// The separate type avoids exposing the ReadTiles and SaveTiles
|
||||
// methods on Client itself.
|
||||
type tileReader struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
func (r *tileReader) Height() int {
|
||||
return r.c.tileHeight
|
||||
}
|
||||
|
||||
// ReadTiles reads and returns the requested tiles,
|
||||
// either from the on-disk cache or the server.
|
||||
func (r *tileReader) ReadTiles(tiles []tlog.Tile) ([][]byte, error) {
|
||||
// Read all the tiles in parallel.
|
||||
data := make([][]byte, len(tiles))
|
||||
errs := make([]error, len(tiles))
|
||||
var wg sync.WaitGroup
|
||||
for i, tile := range tiles {
|
||||
wg.Add(1)
|
||||
go func(i int, tile tlog.Tile) {
|
||||
defer wg.Done()
|
||||
defer func() {
|
||||
if e := recover(); e != nil {
|
||||
errs[i] = fmt.Errorf("panic: %v", e)
|
||||
}
|
||||
}()
|
||||
data[i], errs[i] = r.c.readTile(tile)
|
||||
}(i, tile)
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// tileCacheKey returns the cache key for the tile.
|
||||
func (c *Client) tileCacheKey(tile tlog.Tile) string {
|
||||
return c.name + "/" + tile.Path()
|
||||
}
|
||||
|
||||
// tileRemotePath returns the remote path for the tile.
|
||||
func (c *Client) tileRemotePath(tile tlog.Tile) string {
|
||||
return "/" + tile.Path()
|
||||
}
|
||||
|
||||
// readTile reads a single tile, either from the on-disk cache or the server.
|
||||
func (c *Client) readTile(tile tlog.Tile) ([]byte, error) {
|
||||
type cached struct {
|
||||
data []byte
|
||||
err error
|
||||
}
|
||||
|
||||
result := c.tileCache.Do(tile, func() interface{} {
|
||||
// Try the requested tile in on-disk cache.
|
||||
data, err := c.ops.ReadCache(c.tileCacheKey(tile))
|
||||
if err == nil {
|
||||
c.markTileSaved(tile)
|
||||
return cached{data, nil}
|
||||
}
|
||||
|
||||
// Try the full tile in on-disk cache (if requested tile not already full).
|
||||
// We only save authenticated tiles to the on-disk cache,
|
||||
// so the recreated prefix is equally authenticated.
|
||||
full := tile
|
||||
full.W = 1 << uint(tile.H)
|
||||
if tile != full {
|
||||
data, err := c.ops.ReadCache(c.tileCacheKey(full))
|
||||
if err == nil {
|
||||
c.markTileSaved(tile) // don't save tile later; we already have full
|
||||
return cached{data[:len(data)/full.W*tile.W], nil}
|
||||
}
|
||||
}
|
||||
|
||||
// Try requested tile from server.
|
||||
data, err = c.ops.ReadRemote(c.tileRemotePath(tile))
|
||||
if err == nil {
|
||||
return cached{data, nil}
|
||||
}
|
||||
|
||||
// Try full tile on server.
|
||||
// If the partial tile does not exist, it should be because
|
||||
// the tile has been completed and only the complete one
|
||||
// is available.
|
||||
if tile != full {
|
||||
data, err := c.ops.ReadRemote(c.tileRemotePath(full))
|
||||
if err == nil {
|
||||
// Note: We could save the full tile in the on-disk cache here,
|
||||
// but we don't know if it is valid yet, and we will only find out
|
||||
// about the partial data, not the full data. So let SaveTiles
|
||||
// save the partial tile, and we'll just refetch the full tile later
|
||||
// once we can validate more (or all) of it.
|
||||
return cached{data[:len(data)/full.W*tile.W], nil}
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing worked.
|
||||
// Return the error from the server fetch for the requested (not full) tile.
|
||||
return cached{nil, err}
|
||||
}).(cached)
|
||||
|
||||
return result.data, result.err
|
||||
}
|
||||
|
||||
// markTileSaved records that tile is already present in the on-disk cache,
|
||||
// so that a future SaveTiles for that tile can be ignored.
|
||||
func (c *Client) markTileSaved(tile tlog.Tile) {
|
||||
c.tileSavedMu.Lock()
|
||||
c.tileSaved[tile] = true
|
||||
c.tileSavedMu.Unlock()
|
||||
}
|
||||
|
||||
// SaveTiles saves the now validated tiles.
|
||||
func (r *tileReader) SaveTiles(tiles []tlog.Tile, data [][]byte) {
|
||||
c := r.c
|
||||
|
||||
// Determine which tiles need saving.
|
||||
// (Tiles that came from the cache need not be saved back.)
|
||||
save := make([]bool, len(tiles))
|
||||
c.tileSavedMu.Lock()
|
||||
for i, tile := range tiles {
|
||||
if !c.tileSaved[tile] {
|
||||
save[i] = true
|
||||
c.tileSaved[tile] = true
|
||||
}
|
||||
}
|
||||
c.tileSavedMu.Unlock()
|
||||
|
||||
for i, tile := range tiles {
|
||||
if save[i] {
|
||||
// If WriteCache fails here (out of disk space? i/o error?),
|
||||
// c.tileSaved[tile] is still true and we will not try to write it again.
|
||||
// Next time we run maybe we'll redownload it again and be
|
||||
// more successful.
|
||||
c.ops.WriteCache(c.name+"/"+tile.Path(), data[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,460 @@
|
||||
// Copyright 2019 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 sumdb
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/mod/sumdb/note"
|
||||
"golang.org/x/mod/sumdb/tlog"
|
||||
)
|
||||
|
||||
const (
|
||||
testName = "localhost.localdev/sumdb"
|
||||
testVerifierKey = "localhost.localdev/sumdb+00000c67+AcTrnkbUA+TU4heY3hkjiSES/DSQniBqIeQ/YppAUtK6"
|
||||
testSignerKey = "PRIVATE+KEY+localhost.localdev/sumdb+00000c67+AXu6+oaVaOYuQOFrf1V59JK1owcFlJcHwwXHDfDGxSPk"
|
||||
)
|
||||
|
||||
func TestClientLookup(t *testing.T) {
|
||||
tc := newTestClient(t)
|
||||
tc.mustHaveLatest(1)
|
||||
|
||||
// Basic lookup.
|
||||
tc.mustLookup("rsc.io/sampler", "v1.3.0", "rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=")
|
||||
tc.mustHaveLatest(3)
|
||||
|
||||
// Everything should now be cached, both for the original package and its /go.mod.
|
||||
tc.getOK = false
|
||||
tc.mustLookup("rsc.io/sampler", "v1.3.0", "rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=")
|
||||
tc.mustLookup("rsc.io/sampler", "v1.3.0/go.mod", "rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=")
|
||||
tc.mustHaveLatest(3)
|
||||
tc.getOK = true
|
||||
tc.getTileOK = false // the cache has what we need
|
||||
|
||||
// Lookup with multiple returned lines.
|
||||
tc.mustLookup("rsc.io/quote", "v1.5.2", "rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=\nrsc.io/quote v1.5.2 h2:xyzzy")
|
||||
tc.mustHaveLatest(3)
|
||||
|
||||
// Lookup with need for !-encoding.
|
||||
// rsc.io/Quote is the only record written after rsc.io/samper,
|
||||
// so it is the only one that should need more tiles.
|
||||
tc.getTileOK = true
|
||||
tc.mustLookup("rsc.io/Quote", "v1.5.2", "rsc.io/Quote v1.5.2 h1:uppercase!=")
|
||||
tc.mustHaveLatest(4)
|
||||
}
|
||||
|
||||
func TestClientBadTiles(t *testing.T) {
|
||||
tc := newTestClient(t)
|
||||
|
||||
flipBits := func() {
|
||||
for url, data := range tc.remote {
|
||||
if strings.Contains(url, "/tile/") {
|
||||
for i := range data {
|
||||
data[i] ^= 0x80
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Bad tiles in initial download.
|
||||
tc.mustHaveLatest(1)
|
||||
flipBits()
|
||||
_, err := tc.client.Lookup("rsc.io/sampler", "v1.3.0")
|
||||
tc.mustError(err, "rsc.io/sampler@v1.3.0: initializing sumdb.Client: checking tree#1: downloaded inconsistent tile")
|
||||
flipBits()
|
||||
tc.newClient()
|
||||
tc.mustLookup("rsc.io/sampler", "v1.3.0", "rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=")
|
||||
|
||||
// Bad tiles after initial download.
|
||||
flipBits()
|
||||
_, err = tc.client.Lookup("rsc.io/Quote", "v1.5.2")
|
||||
tc.mustError(err, "rsc.io/Quote@v1.5.2: checking tree#3 against tree#4: downloaded inconsistent tile")
|
||||
flipBits()
|
||||
tc.newClient()
|
||||
tc.mustLookup("rsc.io/Quote", "v1.5.2", "rsc.io/Quote v1.5.2 h1:uppercase!=")
|
||||
|
||||
// Bad starting tree hash looks like bad tiles.
|
||||
tc.newClient()
|
||||
text := tlog.FormatTree(tlog.Tree{N: 1, Hash: tlog.Hash{}})
|
||||
data, err := note.Sign(¬e.Note{Text: string(text)}, tc.signer)
|
||||
if err != nil {
|
||||
tc.t.Fatal(err)
|
||||
}
|
||||
tc.config[testName+"/latest"] = data
|
||||
_, err = tc.client.Lookup("rsc.io/sampler", "v1.3.0")
|
||||
tc.mustError(err, "rsc.io/sampler@v1.3.0: initializing sumdb.Client: checking tree#1: downloaded inconsistent tile")
|
||||
}
|
||||
|
||||
func TestClientFork(t *testing.T) {
|
||||
tc := newTestClient(t)
|
||||
tc2 := tc.fork()
|
||||
|
||||
tc.addRecord("rsc.io/pkg1@v1.5.2", `rsc.io/pkg1 v1.5.2 h1:hash!=
|
||||
`)
|
||||
tc.addRecord("rsc.io/pkg1@v1.5.4", `rsc.io/pkg1 v1.5.4 h1:hash!=
|
||||
`)
|
||||
tc.mustLookup("rsc.io/pkg1", "v1.5.2", "rsc.io/pkg1 v1.5.2 h1:hash!=")
|
||||
|
||||
tc2.addRecord("rsc.io/pkg1@v1.5.3", `rsc.io/pkg1 v1.5.3 h1:hash!=
|
||||
`)
|
||||
tc2.addRecord("rsc.io/pkg1@v1.5.4", `rsc.io/pkg1 v1.5.4 h1:hash!=
|
||||
`)
|
||||
tc2.mustLookup("rsc.io/pkg1", "v1.5.4", "rsc.io/pkg1 v1.5.4 h1:hash!=")
|
||||
|
||||
key := "/lookup/rsc.io/pkg1@v1.5.2"
|
||||
tc2.remote[key] = tc.remote[key]
|
||||
_, err := tc2.client.Lookup("rsc.io/pkg1", "v1.5.2")
|
||||
tc2.mustError(err, ErrSecurity.Error())
|
||||
|
||||
/*
|
||||
SECURITY ERROR
|
||||
go.sum database server misbehavior detected!
|
||||
|
||||
old database:
|
||||
go.sum database tree!
|
||||
5
|
||||
nWzN20+pwMt62p7jbv1/NlN95ePTlHijabv5zO/s36w=
|
||||
|
||||
— localhost.localdev/sumdb AAAMZ5/2FVAdMH58kmnz/0h299pwyskEbzDzoa2/YaPdhvLya4YWDFQQxu2TQb5GpwAH4NdWnTwuhILafisyf3CNbgg=
|
||||
|
||||
new database:
|
||||
go.sum database tree
|
||||
6
|
||||
wc4SkQt52o5W2nQ8To2ARs+mWuUJjss+sdleoiqxMmM=
|
||||
|
||||
— localhost.localdev/sumdb AAAMZ6oRNswlEZ6ZZhxrCvgl1MBy+nusq4JU+TG6Fe2NihWLqOzb+y2c2kzRLoCr4tvw9o36ucQEnhc20e4nA4Qc/wc=
|
||||
|
||||
proof of misbehavior:
|
||||
T7i+H/8ER4nXOiw4Bj0koZOkGjkxoNvlI34GpvhHhQg=
|
||||
Nsuejv72de9hYNM5bqFv8rv3gm3zJQwv/DT/WNbLDLA=
|
||||
mOmqqZ1aI/lzS94oq/JSbj7pD8Rv9S+xDyi12BtVSHo=
|
||||
/7Aw5jVSMM9sFjQhaMg+iiDYPMk6decH7QLOGrL9Lx0=
|
||||
*/
|
||||
|
||||
wants := []string{
|
||||
"SECURITY ERROR",
|
||||
"go.sum database server misbehavior detected!",
|
||||
"old database:\n\tgo.sum database tree\n\t5\n",
|
||||
"— localhost.localdev/sumdb AAAMZ5/2FVAd",
|
||||
"new database:\n\tgo.sum database tree\n\t6\n",
|
||||
"— localhost.localdev/sumdb AAAMZ6oRNswl",
|
||||
"proof of misbehavior:\n\tT7i+H/8ER4nXOiw4Bj0k",
|
||||
}
|
||||
text := tc2.security.String()
|
||||
for _, want := range wants {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("cannot find %q in security text:\n%s", want, text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestClientGONOSUMDB(t *testing.T) {
|
||||
tc := newTestClient(t)
|
||||
tc.client.SetGONOSUMDB("p,*/q")
|
||||
tc.client.Lookup("rsc.io/sampler", "v1.3.0") // initialize before we turn off network
|
||||
tc.getOK = false
|
||||
|
||||
ok := []string{
|
||||
"abc",
|
||||
"a/p",
|
||||
"pq",
|
||||
"q",
|
||||
"n/o/p/q",
|
||||
}
|
||||
skip := []string{
|
||||
"p",
|
||||
"p/x",
|
||||
"x/q",
|
||||
"x/q/z",
|
||||
}
|
||||
|
||||
for _, path := range ok {
|
||||
_, err := tc.client.Lookup(path, "v1.0.0")
|
||||
if err == ErrGONOSUMDB {
|
||||
t.Errorf("Lookup(%q): ErrGONOSUMDB, wanted failed actual lookup", path)
|
||||
}
|
||||
}
|
||||
for _, path := range skip {
|
||||
_, err := tc.client.Lookup(path, "v1.0.0")
|
||||
if err != ErrGONOSUMDB {
|
||||
t.Errorf("Lookup(%q): %v, wanted ErrGONOSUMDB", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A testClient is a self-contained client-side testing environment.
|
||||
type testClient struct {
|
||||
t *testing.T // active test
|
||||
client *Client // client being tested
|
||||
tileHeight int // tile height to use (default 2)
|
||||
getOK bool // should tc.GetURL succeed?
|
||||
getTileOK bool // should tc.GetURL of tiles succeed?
|
||||
treeSize int64
|
||||
hashes []tlog.Hash
|
||||
remote map[string][]byte
|
||||
signer note.Signer
|
||||
|
||||
// mu protects config, cache, log, security
|
||||
// during concurrent use of the exported methods
|
||||
// by the client itself (testClient is the Client's ClientOps,
|
||||
// and the Client methods can both read and write these fields).
|
||||
// Unexported methods invoked directly by the test
|
||||
// (for example, addRecord) need not hold the mutex:
|
||||
// for proper test execution those methods should only
|
||||
// be called when the Client is idle and not using its ClientOps.
|
||||
// Not holding the mutex in those methods ensures
|
||||
// that if a mistake is made, go test -race will report it.
|
||||
// (Holding the mutex would eliminate the race report but
|
||||
// not the underlying problem.)
|
||||
// Similarly, the get map is not protected by the mutex,
|
||||
// because the Client methods only read it.
|
||||
mu sync.Mutex // prot
|
||||
config map[string][]byte
|
||||
cache map[string][]byte
|
||||
security bytes.Buffer
|
||||
}
|
||||
|
||||
// newTestClient returns a new testClient that will call t.Fatal on error
|
||||
// and has a few records already available on the remote server.
|
||||
func newTestClient(t *testing.T) *testClient {
|
||||
tc := &testClient{
|
||||
t: t,
|
||||
tileHeight: 2,
|
||||
getOK: true,
|
||||
getTileOK: true,
|
||||
config: make(map[string][]byte),
|
||||
cache: make(map[string][]byte),
|
||||
remote: make(map[string][]byte),
|
||||
}
|
||||
|
||||
tc.config["key"] = []byte(testVerifierKey + "\n")
|
||||
var err error
|
||||
tc.signer, err = note.NewSigner(testSignerKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tc.newClient()
|
||||
|
||||
tc.addRecord("rsc.io/quote@v1.5.2", `rsc.io/quote v1.5.2 h1:w5fcysjrx7yqtD/aO+QwRjYZOKnaM9Uh2b40tElTs3Y=
|
||||
rsc.io/quote v1.5.2/go.mod h1:LzX7hefJvL54yjefDEDHNONDjII0t9xZLPXsUe+TKr0=
|
||||
rsc.io/quote v1.5.2 h2:xyzzy
|
||||
`)
|
||||
|
||||
tc.addRecord("golang.org/x/text@v0.0.0-20170915032832-14c0d48ead0c", `golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c h1:qgOY6WgZOaTkIIMiVjBQcw93ERBE4m30iBm00nkL0i8=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
`)
|
||||
tc.addRecord("rsc.io/sampler@v1.3.0", `rsc.io/sampler v1.3.0 h1:7uVkIFmeBqHfdjD+gZwtXXI+RODJ2Wc4O7MPEh/QiW4=
|
||||
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
|
||||
`)
|
||||
tc.config[testName+"/latest"] = tc.signTree(1)
|
||||
|
||||
tc.addRecord("rsc.io/!quote@v1.5.2", `rsc.io/Quote v1.5.2 h1:uppercase!=
|
||||
`)
|
||||
return tc
|
||||
}
|
||||
|
||||
// newClient resets the Client associated with tc.
|
||||
// This clears any in-memory cache from the Client
|
||||
// but not tc's on-disk cache.
|
||||
func (tc *testClient) newClient() {
|
||||
tc.client = NewClient(tc)
|
||||
tc.client.SetTileHeight(tc.tileHeight)
|
||||
}
|
||||
|
||||
// mustLookup does a lookup for path@vers and checks that the lines that come back match want.
|
||||
func (tc *testClient) mustLookup(path, vers, want string) {
|
||||
tc.t.Helper()
|
||||
lines, err := tc.client.Lookup(path, vers)
|
||||
if err != nil {
|
||||
tc.t.Fatal(err)
|
||||
}
|
||||
if strings.Join(lines, "\n") != want {
|
||||
tc.t.Fatalf("Lookup(%q, %q):\n\t%s\nwant:\n\t%s", path, vers, strings.Join(lines, "\n\t"), strings.Replace(want, "\n", "\n\t", -1))
|
||||
}
|
||||
}
|
||||
|
||||
// mustHaveLatest checks that the on-disk configuration
|
||||
// for latest is a tree of size n.
|
||||
func (tc *testClient) mustHaveLatest(n int64) {
|
||||
tc.t.Helper()
|
||||
|
||||
latest := tc.config[testName+"/latest"]
|
||||
lines := strings.Split(string(latest), "\n")
|
||||
if len(lines) < 2 || lines[1] != fmt.Sprint(n) {
|
||||
tc.t.Fatalf("/latest should have tree %d, but has:\n%s", n, latest)
|
||||
}
|
||||
}
|
||||
|
||||
// mustError checks that err's error string contains the text.
|
||||
func (tc *testClient) mustError(err error, text string) {
|
||||
tc.t.Helper()
|
||||
if err == nil || !strings.Contains(err.Error(), text) {
|
||||
tc.t.Fatalf("err = %v, want %q", err, text)
|
||||
}
|
||||
}
|
||||
|
||||
// fork returns a copy of tc.
|
||||
// Changes made to the new copy or to tc are not reflected in the other.
|
||||
func (tc *testClient) fork() *testClient {
|
||||
tc2 := &testClient{
|
||||
t: tc.t,
|
||||
getOK: tc.getOK,
|
||||
getTileOK: tc.getTileOK,
|
||||
tileHeight: tc.tileHeight,
|
||||
treeSize: tc.treeSize,
|
||||
hashes: append([]tlog.Hash{}, tc.hashes...),
|
||||
signer: tc.signer,
|
||||
config: copyMap(tc.config),
|
||||
cache: copyMap(tc.cache),
|
||||
remote: copyMap(tc.remote),
|
||||
}
|
||||
tc2.newClient()
|
||||
return tc2
|
||||
}
|
||||
|
||||
func copyMap(m map[string][]byte) map[string][]byte {
|
||||
m2 := make(map[string][]byte)
|
||||
for k, v := range m {
|
||||
m2[k] = v
|
||||
}
|
||||
return m2
|
||||
}
|
||||
|
||||
// ReadHashes is tc's implementation of tlog.HashReader, for use with
|
||||
// tlog.TreeHash and so on.
|
||||
func (tc *testClient) ReadHashes(indexes []int64) ([]tlog.Hash, error) {
|
||||
var list []tlog.Hash
|
||||
for _, id := range indexes {
|
||||
list = append(list, tc.hashes[id])
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
// addRecord adds a log record using the given (!-encoded) key and data.
|
||||
func (tc *testClient) addRecord(key, data string) {
|
||||
tc.t.Helper()
|
||||
|
||||
// Create record, add hashes to log tree.
|
||||
id := tc.treeSize
|
||||
tc.treeSize++
|
||||
rec, err := tlog.FormatRecord(id, []byte(data))
|
||||
if err != nil {
|
||||
tc.t.Fatal(err)
|
||||
}
|
||||
hashes, err := tlog.StoredHashesForRecordHash(id, tlog.RecordHash([]byte(data)), tc)
|
||||
if err != nil {
|
||||
tc.t.Fatal(err)
|
||||
}
|
||||
tc.hashes = append(tc.hashes, hashes...)
|
||||
|
||||
// Create lookup result.
|
||||
tc.remote["/lookup/"+key] = append(rec, tc.signTree(tc.treeSize)...)
|
||||
|
||||
// Create new tiles.
|
||||
tiles := tlog.NewTiles(tc.tileHeight, id, tc.treeSize)
|
||||
for _, tile := range tiles {
|
||||
data, err := tlog.ReadTileData(tile, tc)
|
||||
if err != nil {
|
||||
tc.t.Fatal(err)
|
||||
}
|
||||
tc.remote["/"+tile.Path()] = data
|
||||
// TODO delete old partial tiles
|
||||
}
|
||||
}
|
||||
|
||||
// signTree returns the signed head for the tree of the given size.
|
||||
func (tc *testClient) signTree(size int64) []byte {
|
||||
h, err := tlog.TreeHash(size, tc)
|
||||
if err != nil {
|
||||
tc.t.Fatal(err)
|
||||
}
|
||||
text := tlog.FormatTree(tlog.Tree{N: size, Hash: h})
|
||||
data, err := note.Sign(¬e.Note{Text: string(text)}, tc.signer)
|
||||
if err != nil {
|
||||
tc.t.Fatal(err)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// ReadRemote is for tc's implementation of Client.
|
||||
func (tc *testClient) ReadRemote(path string) ([]byte, error) {
|
||||
// No mutex here because only the Client should be running
|
||||
// and the Client cannot change tc.get.
|
||||
if !tc.getOK {
|
||||
return nil, fmt.Errorf("disallowed remote read %s", path)
|
||||
}
|
||||
if strings.Contains(path, "/tile/") && !tc.getTileOK {
|
||||
return nil, fmt.Errorf("disallowed remote tile read %s", path)
|
||||
}
|
||||
|
||||
data, ok := tc.remote[path]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no remote path %s", path)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// ReadConfig is for tc's implementation of Client.
|
||||
func (tc *testClient) ReadConfig(file string) ([]byte, error) {
|
||||
tc.mu.Lock()
|
||||
defer tc.mu.Unlock()
|
||||
|
||||
data, ok := tc.config[file]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no config %s", file)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// WriteConfig is for tc's implementation of Client.
|
||||
func (tc *testClient) WriteConfig(file string, old, new []byte) error {
|
||||
tc.mu.Lock()
|
||||
defer tc.mu.Unlock()
|
||||
|
||||
data := tc.config[file]
|
||||
if !bytes.Equal(old, data) {
|
||||
return ErrWriteConflict
|
||||
}
|
||||
tc.config[file] = new
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadCache is for tc's implementation of Client.
|
||||
func (tc *testClient) ReadCache(file string) ([]byte, error) {
|
||||
tc.mu.Lock()
|
||||
defer tc.mu.Unlock()
|
||||
|
||||
data, ok := tc.cache[file]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("no cache %s", file)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// WriteCache is for tc's implementation of Client.
|
||||
func (tc *testClient) WriteCache(file string, data []byte) {
|
||||
tc.mu.Lock()
|
||||
defer tc.mu.Unlock()
|
||||
|
||||
tc.cache[file] = data
|
||||
}
|
||||
|
||||
// Log is for tc's implementation of Client.
|
||||
func (tc *testClient) Log(msg string) {
|
||||
tc.t.Log(msg)
|
||||
}
|
||||
|
||||
// SecurityError is for tc's implementation of Client.
|
||||
func (tc *testClient) SecurityError(msg string) {
|
||||
tc.mu.Lock()
|
||||
defer tc.mu.Unlock()
|
||||
|
||||
fmt.Fprintf(&tc.security, "%s\n", strings.TrimRight(msg, "\n"))
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// Copyright 2018 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 dirhash defines hashes over directory trees.
|
||||
// These hashes are recorded in go.sum files and in the Go checksum database,
|
||||
// to allow verifying that a newly-downloaded module has the expected content.
|
||||
package dirhash
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DefaultHash is the default hash function used in new go.sum entries.
|
||||
var DefaultHash Hash = Hash1
|
||||
|
||||
// A Hash is a directory hash function.
|
||||
// It accepts a list of files along with a function that opens the content of each file.
|
||||
// It opens, reads, hashes, and closes each file and returns the overall directory hash.
|
||||
type Hash func(files []string, open func(string) (io.ReadCloser, error)) (string, error)
|
||||
|
||||
// Hash1 is the "h1:" directory hash function, using SHA-256.
|
||||
//
|
||||
// Hash1 is "h1:" followed by the base64-encoded SHA-256 hash of a summary
|
||||
// prepared as if by the Unix command:
|
||||
//
|
||||
// sha256sum $(find . -type f | sort) | sha256sum
|
||||
//
|
||||
// More precisely, the hashed summary contains a single line for each file in the list,
|
||||
// ordered by sort.Strings applied to the file names, where each line consists of
|
||||
// the hexadecimal SHA-256 hash of the file content,
|
||||
// two spaces (U+0020), the file name, and a newline (U+000A).
|
||||
//
|
||||
// File names with newlines (U+000A) are disallowed.
|
||||
func Hash1(files []string, open func(string) (io.ReadCloser, error)) (string, error) {
|
||||
h := sha256.New()
|
||||
files = append([]string(nil), files...)
|
||||
sort.Strings(files)
|
||||
for _, file := range files {
|
||||
if strings.Contains(file, "\n") {
|
||||
return "", errors.New("dirhash: filenames with newlines are not supported")
|
||||
}
|
||||
r, err := open(file)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
hf := sha256.New()
|
||||
_, err = io.Copy(hf, r)
|
||||
r.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Fprintf(h, "%x %s\n", hf.Sum(nil), file)
|
||||
}
|
||||
return "h1:" + base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
// HashDir returns the hash of the local file system directory dir,
|
||||
// replacing the directory name itself with prefix in the file names
|
||||
// used in the hash function.
|
||||
func HashDir(dir, prefix string, hash Hash) (string, error) {
|
||||
files, err := DirFiles(dir, prefix)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
osOpen := func(name string) (io.ReadCloser, error) {
|
||||
return os.Open(filepath.Join(dir, strings.TrimPrefix(name, prefix)))
|
||||
}
|
||||
return hash(files, osOpen)
|
||||
}
|
||||
|
||||
// DirFiles returns the list of files in the tree rooted at dir,
|
||||
// replacing the directory name dir with prefix in each name.
|
||||
// The resulting names always use forward slashes.
|
||||
func DirFiles(dir, prefix string) ([]string, error) {
|
||||
var files []string
|
||||
dir = filepath.Clean(dir)
|
||||
err := filepath.Walk(dir, func(file string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
} else if file == dir {
|
||||
return fmt.Errorf("%s is not a directory", dir)
|
||||
}
|
||||
|
||||
rel := file
|
||||
if dir != "." {
|
||||
rel = file[len(dir)+1:]
|
||||
}
|
||||
f := filepath.Join(prefix, rel)
|
||||
files = append(files, filepath.ToSlash(f))
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
// HashZip returns the hash of the file content in the named zip file.
|
||||
// Only the file names and their contents are included in the hash:
|
||||
// the exact zip file format encoding, compression method,
|
||||
// per-file modification times, and other metadata are ignored.
|
||||
func HashZip(zipfile string, hash Hash) (string, error) {
|
||||
z, err := zip.OpenReader(zipfile)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer z.Close()
|
||||
var files []string
|
||||
zfiles := make(map[string]*zip.File)
|
||||
for _, file := range z.File {
|
||||
files = append(files, file.Name)
|
||||
zfiles[file.Name] = file
|
||||
}
|
||||
zipOpen := func(name string) (io.ReadCloser, error) {
|
||||
f := zfiles[name]
|
||||
if f == nil {
|
||||
return nil, fmt.Errorf("file %q not found in zip", name) // should never happen
|
||||
}
|
||||
return f.Open()
|
||||
}
|
||||
return hash(files, zipOpen)
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
// Copyright 2018 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 dirhash
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func h(s string) string {
|
||||
return fmt.Sprintf("%x", sha256.Sum256([]byte(s)))
|
||||
}
|
||||
|
||||
func htop(k string, s string) string {
|
||||
sum := sha256.Sum256([]byte(s))
|
||||
return k + ":" + base64.StdEncoding.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func TestHash1(t *testing.T) {
|
||||
files := []string{"xyz", "abc"}
|
||||
open := func(name string) (io.ReadCloser, error) {
|
||||
return io.NopCloser(strings.NewReader("data for " + name)), nil
|
||||
}
|
||||
want := htop("h1", fmt.Sprintf("%s %s\n%s %s\n", h("data for abc"), "abc", h("data for xyz"), "xyz"))
|
||||
out, err := Hash1(files, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if out != want {
|
||||
t.Errorf("Hash1(...) = %s, want %s", out, want)
|
||||
}
|
||||
|
||||
_, err = Hash1([]string{"xyz", "a\nbc"}, open)
|
||||
if err == nil {
|
||||
t.Error("Hash1: expected error on newline in filenames")
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashDir(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "xyz"), []byte("data for xyz"), 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "abc"), []byte("data for abc"), 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want := htop("h1", fmt.Sprintf("%s %s\n%s %s\n", h("data for abc"), "prefix/abc", h("data for xyz"), "prefix/xyz"))
|
||||
out, err := HashDir(dir, "prefix", Hash1)
|
||||
if err != nil {
|
||||
t.Fatalf("HashDir: %v", err)
|
||||
}
|
||||
if out != want {
|
||||
t.Errorf("HashDir(...) = %s, want %s", out, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHashZip(t *testing.T) {
|
||||
f, err := os.CreateTemp(t.TempDir(), "dirhash-test-")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
z := zip.NewWriter(f)
|
||||
w, err := z.Create("prefix/xyz")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Write([]byte("data for xyz"))
|
||||
w, err = z.Create("prefix/abc")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Write([]byte("data for abc"))
|
||||
if err := z.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := htop("h1", fmt.Sprintf("%s %s\n%s %s\n", h("data for abc"), "prefix/abc", h("data for xyz"), "prefix/xyz"))
|
||||
out, err := HashZip(f.Name(), Hash1)
|
||||
if err != nil {
|
||||
t.Fatalf("HashDir: %v", err)
|
||||
}
|
||||
if out != want {
|
||||
t.Errorf("HashDir(...) = %s, want %s", out, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirFiles(t *testing.T) {
|
||||
t.Run("valid directory with files", func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "xyz"), []byte("data for xyz"), 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "abc"), []byte("data for abc"), 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.Mkdir(filepath.Join(dir, "subdir"), 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(dir, "subdir", "xyz"), []byte("data for subdir xyz"), 0666); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prefix := "foo/bar@v2.3.4"
|
||||
out, err := DirFiles(dir, prefix)
|
||||
if err != nil {
|
||||
t.Fatalf("DirFiles: %v", err)
|
||||
}
|
||||
for _, file := range out {
|
||||
if !strings.HasPrefix(file, prefix) {
|
||||
t.Errorf("Dir file = %s, want prefix %s", file, prefix)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid directory", func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "not-a-directory.txt")
|
||||
if err := os.WriteFile(path, []byte("This is a file."), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(path)
|
||||
|
||||
out, err := DirFiles(path, "")
|
||||
if err == nil {
|
||||
t.Errorf("DirFiles(...) = %v, expected an error", err)
|
||||
}
|
||||
if len(out) > 0 {
|
||||
t.Errorf("DirFiles(...) = unexpected files %s", out)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright 2019 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 note_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"golang.org/x/mod/sumdb/note"
|
||||
)
|
||||
|
||||
func ExampleSign() {
|
||||
skey := "PRIVATE+KEY+PeterNeumann+c74f20a3+AYEKFALVFGyNhPJEMzD1QIDr+Y7hfZx09iUvxdXHKDFz"
|
||||
text := "If you think cryptography is the answer to your problem,\n" +
|
||||
"then you don't know what your problem is.\n"
|
||||
|
||||
signer, err := note.NewSigner(skey)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
msg, err := note.Sign(¬e.Note{Text: text}, signer)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
os.Stdout.Write(msg)
|
||||
|
||||
// Output:
|
||||
// If you think cryptography is the answer to your problem,
|
||||
// then you don't know what your problem is.
|
||||
//
|
||||
// — PeterNeumann x08go/ZJkuBS9UG/SffcvIAQxVBtiFupLLr8pAcElZInNIuGUgYN1FFYC2pZSNXgKvqfqdngotpRZb6KE6RyyBwJnAM=
|
||||
}
|
||||
|
||||
func ExampleOpen() {
|
||||
vkey := "PeterNeumann+c74f20a3+ARpc2QcUPDhMQegwxbzhKqiBfsVkmqq/LDE4izWy10TW"
|
||||
msg := []byte("If you think cryptography is the answer to your problem,\n" +
|
||||
"then you don't know what your problem is.\n" +
|
||||
"\n" +
|
||||
"— PeterNeumann x08go/ZJkuBS9UG/SffcvIAQxVBtiFupLLr8pAcElZInNIuGUgYN1FFYC2pZSNXgKvqfqdngotpRZb6KE6RyyBwJnAM=\n")
|
||||
|
||||
verifier, err := note.NewVerifier(vkey)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
verifiers := note.VerifierList(verifier)
|
||||
|
||||
n, err := note.Open(msg, verifiers)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s (%08x):\n%s", n.Sigs[0].Name, n.Sigs[0].Hash, n.Text)
|
||||
|
||||
// Output:
|
||||
// PeterNeumann (c74f20a3):
|
||||
// If you think cryptography is the answer to your problem,
|
||||
// then you don't know what your problem is.
|
||||
}
|
||||
|
||||
var rand = struct {
|
||||
Reader io.Reader
|
||||
}{
|
||||
zeroReader{},
|
||||
}
|
||||
|
||||
type zeroReader struct{}
|
||||
|
||||
func (zeroReader) Read(buf []byte) (int, error) {
|
||||
for i := range buf {
|
||||
buf[i] = 0
|
||||
}
|
||||
return len(buf), nil
|
||||
}
|
||||
|
||||
func ExampleSign_add_signatures() {
|
||||
vkey := "PeterNeumann+c74f20a3+ARpc2QcUPDhMQegwxbzhKqiBfsVkmqq/LDE4izWy10TW"
|
||||
msg := []byte("If you think cryptography is the answer to your problem,\n" +
|
||||
"then you don't know what your problem is.\n" +
|
||||
"\n" +
|
||||
"— PeterNeumann x08go/ZJkuBS9UG/SffcvIAQxVBtiFupLLr8pAcElZInNIuGUgYN1FFYC2pZSNXgKvqfqdngotpRZb6KE6RyyBwJnAM=\n")
|
||||
|
||||
verifier, err := note.NewVerifier(vkey)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
verifiers := note.VerifierList(verifier)
|
||||
|
||||
n, err := note.Open(msg, verifiers)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
skey, vkey, err := note.GenerateKey(rand.Reader, "EnochRoot")
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
_ = vkey // give to verifiers
|
||||
|
||||
me, err := note.NewSigner(skey)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
msg, err = note.Sign(n, me)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
os.Stdout.Write(msg)
|
||||
|
||||
// Output:
|
||||
// If you think cryptography is the answer to your problem,
|
||||
// then you don't know what your problem is.
|
||||
//
|
||||
// — PeterNeumann x08go/ZJkuBS9UG/SffcvIAQxVBtiFupLLr8pAcElZInNIuGUgYN1FFYC2pZSNXgKvqfqdngotpRZb6KE6RyyBwJnAM=
|
||||
// — EnochRoot rwz+eBzmZa0SO3NbfRGzPCpDckykFXSdeX+MNtCOXm2/5n2tiOHp+vAF1aGrQ5ovTG01oOTGwnWLox33WWd1RvMc+QQ=
|
||||
}
|
||||
@@ -0,0 +1,678 @@
|
||||
// Copyright 2019 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 note defines the notes signed by the Go module database server.
|
||||
//
|
||||
// A note is text signed by one or more server keys.
|
||||
// The text should be ignored unless the note is signed by
|
||||
// a trusted server key and the signature has been verified
|
||||
// using the server's public key.
|
||||
//
|
||||
// A server's public key is identified by a name, typically the "host[/path]"
|
||||
// giving the base URL of the server's transparency log.
|
||||
// The syntactic restrictions on a name are that it be non-empty,
|
||||
// well-formed UTF-8 containing neither Unicode spaces nor plus (U+002B).
|
||||
//
|
||||
// A Go module database server signs texts using public key cryptography.
|
||||
// A given server may have multiple public keys, each
|
||||
// identified by a 32-bit hash of the public key.
|
||||
//
|
||||
// # Verifying Notes
|
||||
//
|
||||
// A [Verifier] allows verification of signatures by one server public key.
|
||||
// It can report the name of the server and the uint32 hash of the key,
|
||||
// and it can verify a purported signature by that key.
|
||||
//
|
||||
// The standard implementation of a Verifier is constructed
|
||||
// by [NewVerifier] starting from a verifier key, which is a
|
||||
// plain text string of the form "<name>+<hash>+<keydata>".
|
||||
//
|
||||
// A [Verifiers] allows looking up a Verifier by the combination
|
||||
// of server name and key hash.
|
||||
//
|
||||
// The standard implementation of a Verifiers is constructed
|
||||
// by VerifierList from a list of known verifiers.
|
||||
//
|
||||
// A [Note] represents a text with one or more signatures.
|
||||
// An implementation can reject a note with too many signatures
|
||||
// (for example, more than 100 signatures).
|
||||
//
|
||||
// A [Signature] represents a signature on a note, verified or not.
|
||||
//
|
||||
// The [Open] function takes as input a signed message
|
||||
// and a set of known verifiers. It decodes and verifies
|
||||
// the message signatures and returns a [Note] structure
|
||||
// containing the message text and (verified or unverified) signatures.
|
||||
//
|
||||
// # Signing Notes
|
||||
//
|
||||
// A [Signer] allows signing a text with a given key.
|
||||
// It can report the name of the server and the hash of the key
|
||||
// and can sign a raw text using that key.
|
||||
//
|
||||
// The standard implementation of a Signer is constructed
|
||||
// by [NewSigner] starting from an encoded signer key, which is a
|
||||
// plain text string of the form "PRIVATE+KEY+<name>+<hash>+<keydata>".
|
||||
// Anyone with an encoded signer key can sign messages using that key,
|
||||
// so it must be kept secret. The encoding begins with the literal text
|
||||
// "PRIVATE+KEY" to avoid confusion with the public server key.
|
||||
//
|
||||
// The [Sign] function takes as input a Note and a list of Signers
|
||||
// and returns an encoded, signed message.
|
||||
//
|
||||
// # Signed Note Format
|
||||
//
|
||||
// A signed note consists of a text ending in newline (U+000A),
|
||||
// followed by a blank line (only a newline),
|
||||
// followed by one or more signature lines of this form:
|
||||
// em dash (U+2014), space (U+0020),
|
||||
// server name, space, base64-encoded signature, newline.
|
||||
//
|
||||
// Signed notes must be valid UTF-8 and must not contain any
|
||||
// ASCII control characters (those below U+0020) other than newline.
|
||||
//
|
||||
// A signature is a base64 encoding of 4+n bytes.
|
||||
//
|
||||
// The first four bytes in the signature are the uint32 key hash
|
||||
// stored in big-endian order.
|
||||
//
|
||||
// The remaining n bytes are the result of using the specified key
|
||||
// to sign the note text (including the final newline but not the
|
||||
// separating blank line).
|
||||
//
|
||||
// # Generating Keys
|
||||
//
|
||||
// There is only one key type, Ed25519 with algorithm identifier 1.
|
||||
// New key types may be introduced in the future as needed,
|
||||
// although doing so will require deploying the new algorithms to all clients
|
||||
// before starting to depend on them for signatures.
|
||||
//
|
||||
// The [GenerateKey] function generates and returns a new signer
|
||||
// and corresponding verifier.
|
||||
//
|
||||
// # Example
|
||||
//
|
||||
// Here is a well-formed signed note:
|
||||
//
|
||||
// If you think cryptography is the answer to your problem,
|
||||
// then you don't know what your problem is.
|
||||
//
|
||||
// — PeterNeumann x08go/ZJkuBS9UG/SffcvIAQxVBtiFupLLr8pAcElZInNIuGUgYN1FFYC2pZSNXgKvqfqdngotpRZb6KE6RyyBwJnAM=
|
||||
//
|
||||
// It can be constructed and displayed using:
|
||||
//
|
||||
// skey := "PRIVATE+KEY+PeterNeumann+c74f20a3+AYEKFALVFGyNhPJEMzD1QIDr+Y7hfZx09iUvxdXHKDFz"
|
||||
// text := "If you think cryptography is the answer to your problem,\n" +
|
||||
// "then you don't know what your problem is.\n"
|
||||
//
|
||||
// signer, err := note.NewSigner(skey)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// msg, err := note.Sign(¬e.Note{Text: text}, signer)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// os.Stdout.Write(msg)
|
||||
//
|
||||
// The note's text is two lines, including the final newline,
|
||||
// and the text is purportedly signed by a server named
|
||||
// "PeterNeumann". (Although server names are canonically
|
||||
// base URLs, the only syntactic requirement is that they
|
||||
// not contain spaces or newlines).
|
||||
//
|
||||
// If [Open] is given access to a [Verifiers] including the
|
||||
// [Verifier] for this key, then it will succeed at verifying
|
||||
// the encoded message and returning the parsed [Note]:
|
||||
//
|
||||
// vkey := "PeterNeumann+c74f20a3+ARpc2QcUPDhMQegwxbzhKqiBfsVkmqq/LDE4izWy10TW"
|
||||
// msg := []byte("If you think cryptography is the answer to your problem,\n" +
|
||||
// "then you don't know what your problem is.\n" +
|
||||
// "\n" +
|
||||
// "— PeterNeumann x08go/ZJkuBS9UG/SffcvIAQxVBtiFupLLr8pAcElZInNIuGUgYN1FFYC2pZSNXgKvqfqdngotpRZb6KE6RyyBwJnAM=\n")
|
||||
//
|
||||
// verifier, err := note.NewVerifier(vkey)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// verifiers := note.VerifierList(verifier)
|
||||
//
|
||||
// n, err := note.Open([]byte(msg), verifiers)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// fmt.Printf("%s (%08x):\n%s", n.Sigs[0].Name, n.Sigs[0].Hash, n.Text)
|
||||
//
|
||||
// You can add your own signature to this message by re-signing the note:
|
||||
//
|
||||
// skey, vkey, err := note.GenerateKey(rand.Reader, "EnochRoot")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// _ = vkey // give to verifiers
|
||||
//
|
||||
// me, err := note.NewSigner(skey)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// msg, err := note.Sign(n, me)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// os.Stdout.Write(msg)
|
||||
//
|
||||
// This will print a doubly-signed message, like:
|
||||
//
|
||||
// If you think cryptography is the answer to your problem,
|
||||
// then you don't know what your problem is.
|
||||
//
|
||||
// — PeterNeumann x08go/ZJkuBS9UG/SffcvIAQxVBtiFupLLr8pAcElZInNIuGUgYN1FFYC2pZSNXgKvqfqdngotpRZb6KE6RyyBwJnAM=
|
||||
// — EnochRoot rwz+eBzmZa0SO3NbfRGzPCpDckykFXSdeX+MNtCOXm2/5n2tiOHp+vAF1aGrQ5ovTG01oOTGwnWLox33WWd1RvMc+QQ=
|
||||
package note
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ed25519"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// A Verifier verifies messages signed with a specific key.
|
||||
type Verifier interface {
|
||||
// Name returns the server name associated with the key.
|
||||
Name() string
|
||||
|
||||
// KeyHash returns the key hash.
|
||||
KeyHash() uint32
|
||||
|
||||
// Verify reports whether sig is a valid signature of msg.
|
||||
Verify(msg, sig []byte) bool
|
||||
}
|
||||
|
||||
// A Signer signs messages using a specific key.
|
||||
type Signer interface {
|
||||
// Name returns the server name associated with the key.
|
||||
Name() string
|
||||
|
||||
// KeyHash returns the key hash.
|
||||
KeyHash() uint32
|
||||
|
||||
// Sign returns a signature for the given message.
|
||||
Sign(msg []byte) ([]byte, error)
|
||||
}
|
||||
|
||||
// keyHash computes the key hash for the given server name and encoded public key.
|
||||
func keyHash(name string, key []byte) uint32 {
|
||||
h := sha256.New()
|
||||
h.Write([]byte(name))
|
||||
h.Write([]byte("\n"))
|
||||
h.Write(key)
|
||||
sum := h.Sum(nil)
|
||||
return binary.BigEndian.Uint32(sum)
|
||||
}
|
||||
|
||||
var (
|
||||
errVerifierID = errors.New("malformed verifier id")
|
||||
errVerifierAlg = errors.New("unknown verifier algorithm")
|
||||
errVerifierHash = errors.New("invalid verifier hash")
|
||||
)
|
||||
|
||||
const (
|
||||
algEd25519 = 1
|
||||
)
|
||||
|
||||
// isValidName reports whether name is valid.
|
||||
// It must be non-empty and not have any Unicode spaces or pluses.
|
||||
func isValidName(name string) bool {
|
||||
return name != "" && utf8.ValidString(name) && strings.IndexFunc(name, unicode.IsSpace) < 0 && !strings.Contains(name, "+")
|
||||
}
|
||||
|
||||
// NewVerifier construct a new [Verifier] from an encoded verifier key.
|
||||
func NewVerifier(vkey string) (Verifier, error) {
|
||||
name, vkey := chop(vkey, "+")
|
||||
hash16, key64 := chop(vkey, "+")
|
||||
hash, err1 := strconv.ParseUint(hash16, 16, 32)
|
||||
key, err2 := base64.StdEncoding.DecodeString(key64)
|
||||
if len(hash16) != 8 || err1 != nil || err2 != nil || !isValidName(name) || len(key) == 0 {
|
||||
return nil, errVerifierID
|
||||
}
|
||||
if uint32(hash) != keyHash(name, key) {
|
||||
return nil, errVerifierHash
|
||||
}
|
||||
|
||||
v := &verifier{
|
||||
name: name,
|
||||
hash: uint32(hash),
|
||||
}
|
||||
|
||||
alg, key := key[0], key[1:]
|
||||
switch alg {
|
||||
default:
|
||||
return nil, errVerifierAlg
|
||||
|
||||
case algEd25519:
|
||||
if len(key) != 32 {
|
||||
return nil, errVerifierID
|
||||
}
|
||||
v.verify = func(msg, sig []byte) bool {
|
||||
return ed25519.Verify(key, msg, sig)
|
||||
}
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
// chop chops s at the first instance of sep, if any,
|
||||
// and returns the text before and after sep.
|
||||
// If sep is not present, chop returns before is s and after is empty.
|
||||
func chop(s, sep string) (before, after string) {
|
||||
i := strings.Index(s, sep)
|
||||
if i < 0 {
|
||||
return s, ""
|
||||
}
|
||||
return s[:i], s[i+len(sep):]
|
||||
}
|
||||
|
||||
// verifier is a trivial Verifier implementation.
|
||||
type verifier struct {
|
||||
name string
|
||||
hash uint32
|
||||
verify func([]byte, []byte) bool
|
||||
}
|
||||
|
||||
func (v *verifier) Name() string { return v.name }
|
||||
func (v *verifier) KeyHash() uint32 { return v.hash }
|
||||
func (v *verifier) Verify(msg, sig []byte) bool { return v.verify(msg, sig) }
|
||||
|
||||
// NewSigner constructs a new [Signer] from an encoded signer key.
|
||||
func NewSigner(skey string) (Signer, error) {
|
||||
priv1, skey := chop(skey, "+")
|
||||
priv2, skey := chop(skey, "+")
|
||||
name, skey := chop(skey, "+")
|
||||
hash16, key64 := chop(skey, "+")
|
||||
hash, err1 := strconv.ParseUint(hash16, 16, 32)
|
||||
key, err2 := base64.StdEncoding.DecodeString(key64)
|
||||
if priv1 != "PRIVATE" || priv2 != "KEY" || len(hash16) != 8 || err1 != nil || err2 != nil || !isValidName(name) || len(key) == 0 {
|
||||
return nil, errSignerID
|
||||
}
|
||||
|
||||
// Note: hash is the hash of the public key and we have the private key.
|
||||
// Must verify hash after deriving public key.
|
||||
|
||||
s := &signer{
|
||||
name: name,
|
||||
hash: uint32(hash),
|
||||
}
|
||||
|
||||
var pubkey []byte
|
||||
|
||||
alg, key := key[0], key[1:]
|
||||
switch alg {
|
||||
default:
|
||||
return nil, errSignerAlg
|
||||
|
||||
case algEd25519:
|
||||
if len(key) != 32 {
|
||||
return nil, errSignerID
|
||||
}
|
||||
key = ed25519.NewKeyFromSeed(key)
|
||||
pubkey = append([]byte{algEd25519}, key[32:]...)
|
||||
s.sign = func(msg []byte) ([]byte, error) {
|
||||
return ed25519.Sign(key, msg), nil
|
||||
}
|
||||
}
|
||||
|
||||
if uint32(hash) != keyHash(name, pubkey) {
|
||||
return nil, errSignerHash
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
var (
|
||||
errSignerID = errors.New("malformed verifier id")
|
||||
errSignerAlg = errors.New("unknown verifier algorithm")
|
||||
errSignerHash = errors.New("invalid verifier hash")
|
||||
)
|
||||
|
||||
// signer is a trivial Signer implementation.
|
||||
type signer struct {
|
||||
name string
|
||||
hash uint32
|
||||
sign func([]byte) ([]byte, error)
|
||||
}
|
||||
|
||||
func (s *signer) Name() string { return s.name }
|
||||
func (s *signer) KeyHash() uint32 { return s.hash }
|
||||
func (s *signer) Sign(msg []byte) ([]byte, error) { return s.sign(msg) }
|
||||
|
||||
// GenerateKey generates a signer and verifier key pair for a named server.
|
||||
// The signer key skey is private and must be kept secret.
|
||||
func GenerateKey(rand io.Reader, name string) (skey, vkey string, err error) {
|
||||
pub, priv, err := ed25519.GenerateKey(rand)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
pubkey := append([]byte{algEd25519}, pub...)
|
||||
privkey := append([]byte{algEd25519}, priv.Seed()...)
|
||||
h := keyHash(name, pubkey)
|
||||
|
||||
skey = fmt.Sprintf("PRIVATE+KEY+%s+%08x+%s", name, h, base64.StdEncoding.EncodeToString(privkey))
|
||||
vkey = fmt.Sprintf("%s+%08x+%s", name, h, base64.StdEncoding.EncodeToString(pubkey))
|
||||
return skey, vkey, nil
|
||||
}
|
||||
|
||||
// NewEd25519VerifierKey returns an encoded verifier key using the given name
|
||||
// and Ed25519 public key.
|
||||
func NewEd25519VerifierKey(name string, key ed25519.PublicKey) (string, error) {
|
||||
if len(key) != ed25519.PublicKeySize {
|
||||
return "", fmt.Errorf("invalid public key size %d, expected %d", len(key), ed25519.PublicKeySize)
|
||||
}
|
||||
|
||||
pubkey := append([]byte{algEd25519}, key...)
|
||||
hash := keyHash(name, pubkey)
|
||||
|
||||
b64Key := base64.StdEncoding.EncodeToString(pubkey)
|
||||
return fmt.Sprintf("%s+%08x+%s", name, hash, b64Key), nil
|
||||
}
|
||||
|
||||
// A Verifiers is a collection of known verifier keys.
|
||||
type Verifiers interface {
|
||||
// Verifier returns the Verifier associated with the key
|
||||
// identified by the name and hash.
|
||||
// If the name, hash pair is unknown, Verifier should return
|
||||
// an UnknownVerifierError.
|
||||
Verifier(name string, hash uint32) (Verifier, error)
|
||||
}
|
||||
|
||||
// An UnknownVerifierError indicates that the given key is not known.
|
||||
// The Open function records signatures without associated verifiers as
|
||||
// unverified signatures.
|
||||
type UnknownVerifierError struct {
|
||||
Name string
|
||||
KeyHash uint32
|
||||
}
|
||||
|
||||
func (e *UnknownVerifierError) Error() string {
|
||||
return fmt.Sprintf("unknown key %s+%08x", e.Name, e.KeyHash)
|
||||
}
|
||||
|
||||
// An ambiguousVerifierError indicates that the given name and hash
|
||||
// match multiple keys passed to [VerifierList].
|
||||
// (If this happens, some malicious actor has taken control of the
|
||||
// verifier list, at which point we may as well give up entirely,
|
||||
// but we diagnose the problem instead.)
|
||||
type ambiguousVerifierError struct {
|
||||
name string
|
||||
hash uint32
|
||||
}
|
||||
|
||||
func (e *ambiguousVerifierError) Error() string {
|
||||
return fmt.Sprintf("ambiguous key %s+%08x", e.name, e.hash)
|
||||
}
|
||||
|
||||
// VerifierList returns a [Verifiers] implementation that uses the given list of verifiers.
|
||||
func VerifierList(list ...Verifier) Verifiers {
|
||||
m := make(verifierMap)
|
||||
for _, v := range list {
|
||||
k := nameHash{v.Name(), v.KeyHash()}
|
||||
m[k] = append(m[k], v)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
type nameHash struct {
|
||||
name string
|
||||
hash uint32
|
||||
}
|
||||
|
||||
type verifierMap map[nameHash][]Verifier
|
||||
|
||||
func (m verifierMap) Verifier(name string, hash uint32) (Verifier, error) {
|
||||
v, ok := m[nameHash{name, hash}]
|
||||
if !ok {
|
||||
return nil, &UnknownVerifierError{name, hash}
|
||||
}
|
||||
if len(v) > 1 {
|
||||
return nil, &ambiguousVerifierError{name, hash}
|
||||
}
|
||||
return v[0], nil
|
||||
}
|
||||
|
||||
// A Note is a text and signatures.
|
||||
type Note struct {
|
||||
Text string // text of note
|
||||
Sigs []Signature // verified signatures
|
||||
UnverifiedSigs []Signature // unverified signatures
|
||||
}
|
||||
|
||||
// A Signature is a single signature found in a note.
|
||||
type Signature struct {
|
||||
// Name and Hash give the name and key hash
|
||||
// for the key that generated the signature.
|
||||
Name string
|
||||
Hash uint32
|
||||
|
||||
// Base64 records the base64-encoded signature bytes.
|
||||
Base64 string
|
||||
}
|
||||
|
||||
// An UnverifiedNoteError indicates that the note
|
||||
// successfully parsed but had no verifiable signatures.
|
||||
type UnverifiedNoteError struct {
|
||||
Note *Note
|
||||
}
|
||||
|
||||
func (e *UnverifiedNoteError) Error() string {
|
||||
return "note has no verifiable signatures"
|
||||
}
|
||||
|
||||
// An InvalidSignatureError indicates that the given key was known
|
||||
// and the associated Verifier rejected the signature.
|
||||
type InvalidSignatureError struct {
|
||||
Name string
|
||||
Hash uint32
|
||||
}
|
||||
|
||||
func (e *InvalidSignatureError) Error() string {
|
||||
return fmt.Sprintf("invalid signature for key %s+%08x", e.Name, e.Hash)
|
||||
}
|
||||
|
||||
var (
|
||||
errMalformedNote = errors.New("malformed note")
|
||||
errInvalidSigner = errors.New("invalid signer")
|
||||
errMismatchedVerifier = errors.New("verifier name or hash doesn't match signature")
|
||||
|
||||
sigSplit = []byte("\n\n")
|
||||
sigPrefix = []byte("— ")
|
||||
)
|
||||
|
||||
// Open opens and parses the message msg, checking signatures from the known verifiers.
|
||||
//
|
||||
// For each signature in the message, Open calls known.Verifier to find a verifier.
|
||||
// If known.Verifier returns a verifier and the verifier accepts the signature,
|
||||
// Open records the signature in the returned note's Sigs field.
|
||||
// If known.Verifier returns a verifier but the verifier rejects the signature,
|
||||
// Open returns an InvalidSignatureError.
|
||||
// If known.Verifier returns an UnknownVerifierError,
|
||||
// Open records the signature in the returned note's UnverifiedSigs field.
|
||||
// If known.Verifier returns any other error, Open returns that error.
|
||||
//
|
||||
// If no known verifier has signed an otherwise valid note,
|
||||
// Open returns an [UnverifiedNoteError].
|
||||
// In this case, the unverified note can be fetched from inside the error.
|
||||
func Open(msg []byte, known Verifiers) (*Note, error) {
|
||||
if known == nil {
|
||||
// Treat nil Verifiers as empty list, to produce useful error instead of crash.
|
||||
known = VerifierList()
|
||||
}
|
||||
|
||||
// Must have valid UTF-8 with no non-newline ASCII control characters.
|
||||
for i := 0; i < len(msg); {
|
||||
r, size := utf8.DecodeRune(msg[i:])
|
||||
if r < 0x20 && r != '\n' || r == utf8.RuneError && size == 1 {
|
||||
return nil, errMalformedNote
|
||||
}
|
||||
i += size
|
||||
}
|
||||
|
||||
// Must end with signature block preceded by blank line.
|
||||
split := bytes.LastIndex(msg, sigSplit)
|
||||
if split < 0 {
|
||||
return nil, errMalformedNote
|
||||
}
|
||||
text, sigs := msg[:split+1], msg[split+2:]
|
||||
if len(sigs) == 0 || sigs[len(sigs)-1] != '\n' {
|
||||
return nil, errMalformedNote
|
||||
}
|
||||
|
||||
n := &Note{
|
||||
Text: string(text),
|
||||
}
|
||||
|
||||
// Parse and verify signatures.
|
||||
// Ignore duplicate signatures.
|
||||
seen := make(map[nameHash]bool)
|
||||
seenUnverified := make(map[string]bool)
|
||||
numSig := 0
|
||||
for len(sigs) > 0 {
|
||||
// Pull out next signature line.
|
||||
// We know sigs[len(sigs)-1] == '\n', so IndexByte always finds one.
|
||||
i := bytes.IndexByte(sigs, '\n')
|
||||
line := sigs[:i]
|
||||
sigs = sigs[i+1:]
|
||||
|
||||
if !bytes.HasPrefix(line, sigPrefix) {
|
||||
return nil, errMalformedNote
|
||||
}
|
||||
line = line[len(sigPrefix):]
|
||||
name, b64 := chop(string(line), " ")
|
||||
sig, err := base64.StdEncoding.DecodeString(b64)
|
||||
if err != nil || !isValidName(name) || b64 == "" || len(sig) < 5 {
|
||||
return nil, errMalformedNote
|
||||
}
|
||||
hash := binary.BigEndian.Uint32(sig[0:4])
|
||||
sig = sig[4:]
|
||||
|
||||
if numSig++; numSig > 100 {
|
||||
// Avoid spending forever parsing a note with many signatures.
|
||||
return nil, errMalformedNote
|
||||
}
|
||||
|
||||
v, err := known.Verifier(name, hash)
|
||||
if _, ok := err.(*UnknownVerifierError); ok {
|
||||
// Drop repeated identical unverified signatures.
|
||||
if seenUnverified[string(line)] {
|
||||
continue
|
||||
}
|
||||
seenUnverified[string(line)] = true
|
||||
n.UnverifiedSigs = append(n.UnverifiedSigs, Signature{Name: name, Hash: hash, Base64: b64})
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check that known.Verifier returned the right verifier.
|
||||
if v.Name() != name || v.KeyHash() != hash {
|
||||
return nil, errMismatchedVerifier
|
||||
}
|
||||
|
||||
// Drop repeated signatures by a single verifier.
|
||||
if seen[nameHash{name, hash}] {
|
||||
continue
|
||||
}
|
||||
seen[nameHash{name, hash}] = true
|
||||
|
||||
ok := v.Verify(text, sig)
|
||||
if !ok {
|
||||
return nil, &InvalidSignatureError{name, hash}
|
||||
}
|
||||
|
||||
n.Sigs = append(n.Sigs, Signature{Name: name, Hash: hash, Base64: b64})
|
||||
}
|
||||
|
||||
// Parsed and verified all the signatures.
|
||||
if len(n.Sigs) == 0 {
|
||||
return nil, &UnverifiedNoteError{n}
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
// Sign signs the note with the given signers and returns the encoded message.
|
||||
// The new signatures from signers are listed in the encoded message after
|
||||
// the existing signatures already present in n.Sigs.
|
||||
// If any signer uses the same key as an existing signature,
|
||||
// the existing signature is elided from the output.
|
||||
func Sign(n *Note, signers ...Signer) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
if !strings.HasSuffix(n.Text, "\n") {
|
||||
return nil, errMalformedNote
|
||||
}
|
||||
buf.WriteString(n.Text)
|
||||
|
||||
// Prepare signatures.
|
||||
var sigs bytes.Buffer
|
||||
have := make(map[nameHash]bool)
|
||||
for _, s := range signers {
|
||||
name := s.Name()
|
||||
hash := s.KeyHash()
|
||||
have[nameHash{name, hash}] = true
|
||||
if !isValidName(name) {
|
||||
return nil, errInvalidSigner
|
||||
}
|
||||
|
||||
sig, err := s.Sign(buf.Bytes()) // buf holds n.Text
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var hbuf [4]byte
|
||||
binary.BigEndian.PutUint32(hbuf[:], hash)
|
||||
b64 := base64.StdEncoding.EncodeToString(append(hbuf[:], sig...))
|
||||
sigs.WriteString("— ")
|
||||
sigs.WriteString(name)
|
||||
sigs.WriteString(" ")
|
||||
sigs.WriteString(b64)
|
||||
sigs.WriteString("\n")
|
||||
}
|
||||
|
||||
buf.WriteString("\n")
|
||||
|
||||
// Emit existing signatures not replaced by new ones.
|
||||
for _, list := range [][]Signature{n.Sigs, n.UnverifiedSigs} {
|
||||
for _, sig := range list {
|
||||
name, hash := sig.Name, sig.Hash
|
||||
if !isValidName(name) {
|
||||
return nil, errMalformedNote
|
||||
}
|
||||
if have[nameHash{name, hash}] {
|
||||
continue
|
||||
}
|
||||
// Double-check hash against base64.
|
||||
raw, err := base64.StdEncoding.DecodeString(sig.Base64)
|
||||
if err != nil || len(raw) < 4 || binary.BigEndian.Uint32(raw) != hash {
|
||||
return nil, errMalformedNote
|
||||
}
|
||||
buf.WriteString("— ")
|
||||
buf.WriteString(sig.Name)
|
||||
buf.WriteString(" ")
|
||||
buf.WriteString(sig.Base64)
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
}
|
||||
buf.Write(sigs.Bytes())
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
@@ -0,0 +1,490 @@
|
||||
// Copyright 2019 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 note
|
||||
|
||||
import (
|
||||
"crypto/ed25519"
|
||||
"crypto/rand"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/iotest"
|
||||
)
|
||||
|
||||
func TestNewVerifier(t *testing.T) {
|
||||
vkey := "PeterNeumann+c74f20a3+ARpc2QcUPDhMQegwxbzhKqiBfsVkmqq/LDE4izWy10TW"
|
||||
_, err := NewVerifier(vkey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check various manglings are not accepted.
|
||||
badKey := func(k string) {
|
||||
_, err := NewVerifier(k)
|
||||
if err == nil {
|
||||
t.Errorf("NewVerifier(%q) succeeded, should have failed", k)
|
||||
}
|
||||
}
|
||||
|
||||
b := []byte(vkey)
|
||||
for i := 0; i <= len(b); i++ {
|
||||
for j := i + 1; j <= len(b); j++ {
|
||||
if i != 0 || j != len(b) {
|
||||
badKey(string(b[i:j]))
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := 0; i < len(b); i++ {
|
||||
b[i]++
|
||||
badKey(string(b))
|
||||
b[i]--
|
||||
}
|
||||
|
||||
badKey("PeterNeumann+cc469956+ARpc2QcUPDhMQegwxbzhKqiBfsVkmqq/LDE4izWy10TWBADKEY==") // wrong length key, with adjusted key hash
|
||||
badKey("PeterNeumann+173116ae+ZRpc2QcUPDhMQegwxbzhKqiBfsVkmqq/LDE4izWy10TW") // unknown algorithm, with adjusted key hash
|
||||
}
|
||||
|
||||
func TestNewSigner(t *testing.T) {
|
||||
skey := "PRIVATE+KEY+PeterNeumann+c74f20a3+AYEKFALVFGyNhPJEMzD1QIDr+Y7hfZx09iUvxdXHKDFz"
|
||||
_, err := NewSigner(skey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check various manglings are not accepted.
|
||||
b := []byte(skey)
|
||||
for i := 0; i <= len(b); i++ {
|
||||
for j := i + 1; j <= len(b); j++ {
|
||||
if i == 0 && j == len(b) {
|
||||
continue
|
||||
}
|
||||
_, err := NewSigner(string(b[i:j]))
|
||||
if err == nil {
|
||||
t.Errorf("NewSigner(%q) succeeded, should have failed", b[i:j])
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := 0; i < len(b); i++ {
|
||||
b[i]++
|
||||
_, err := NewSigner(string(b))
|
||||
if err == nil {
|
||||
t.Errorf("NewSigner(%q) succeeded, should have failed", b)
|
||||
}
|
||||
b[i]--
|
||||
}
|
||||
}
|
||||
|
||||
func testSignerAndVerifier(t *testing.T, Name string, signer Signer, verifier Verifier) {
|
||||
if name := signer.Name(); name != Name {
|
||||
t.Errorf("signer.Name() = %q, want %q", name, Name)
|
||||
}
|
||||
if name := verifier.Name(); name != Name {
|
||||
t.Errorf("verifier.Name() = %q, want %q", name, Name)
|
||||
}
|
||||
shash := signer.KeyHash()
|
||||
vhash := verifier.KeyHash()
|
||||
if shash != vhash {
|
||||
t.Errorf("signer.KeyHash() = %#08x != verifier.KeyHash() = %#08x", shash, vhash)
|
||||
}
|
||||
|
||||
msg := []byte("hi")
|
||||
sig, err := signer.Sign(msg)
|
||||
if err != nil {
|
||||
t.Fatalf("signer.Sign: %v", err)
|
||||
}
|
||||
if !verifier.Verify(msg, sig) {
|
||||
t.Fatalf("verifier.Verify failed on signature returned by signer.Sign")
|
||||
}
|
||||
sig[0]++
|
||||
if verifier.Verify(msg, sig) {
|
||||
t.Fatalf("verifier.Verify succeeded on corrupt signature")
|
||||
}
|
||||
sig[0]--
|
||||
msg[0]++
|
||||
if verifier.Verify(msg, sig) {
|
||||
t.Fatalf("verifier.Verify succeeded on corrupt message")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateKey(t *testing.T) {
|
||||
// Generate key pair, make sure it is all self-consistent.
|
||||
const Name = "EnochRoot"
|
||||
|
||||
skey, vkey, err := GenerateKey(rand.Reader, Name)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKey: %v", err)
|
||||
}
|
||||
signer, err := NewSigner(skey)
|
||||
if err != nil {
|
||||
t.Fatalf("NewSigner: %v", err)
|
||||
}
|
||||
verifier, err := NewVerifier(vkey)
|
||||
if err != nil {
|
||||
t.Fatalf("NewVerifier: %v", err)
|
||||
}
|
||||
|
||||
testSignerAndVerifier(t, Name, signer, verifier)
|
||||
|
||||
// Check that GenerateKey returns error from rand reader.
|
||||
_, _, err = GenerateKey(iotest.TimeoutReader(iotest.OneByteReader(rand.Reader)), Name)
|
||||
if err == nil {
|
||||
t.Fatalf("GenerateKey succeeded with error-returning rand reader")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromEd25519(t *testing.T) {
|
||||
const Name = "EnochRoot"
|
||||
|
||||
pub, priv, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("GenerateKey: %v", err)
|
||||
}
|
||||
signer, err := newSignerFromEd25519Seed(Name, priv.Seed())
|
||||
if err != nil {
|
||||
t.Fatalf("newSignerFromEd25519Seed: %v", err)
|
||||
}
|
||||
vkey, err := NewEd25519VerifierKey(Name, pub)
|
||||
if err != nil {
|
||||
t.Fatalf("NewEd25519VerifierKey: %v", err)
|
||||
}
|
||||
verifier, err := NewVerifier(vkey)
|
||||
if err != nil {
|
||||
t.Fatalf("NewVerifier: %v", err)
|
||||
}
|
||||
|
||||
testSignerAndVerifier(t, Name, signer, verifier)
|
||||
|
||||
// Check that wrong key sizes return errors.
|
||||
_, err = NewEd25519VerifierKey(Name, pub[:len(pub)-1])
|
||||
if err == nil {
|
||||
t.Errorf("NewEd25519VerifierKey succeeded with a seed of the wrong size")
|
||||
}
|
||||
}
|
||||
|
||||
// newSignerFromEd25519Seed constructs a new signer from a verifier name and a
|
||||
// crypto/ed25519 private key seed.
|
||||
func newSignerFromEd25519Seed(name string, seed []byte) (Signer, error) {
|
||||
if len(seed) != ed25519.SeedSize {
|
||||
return nil, errors.New("invalid seed size")
|
||||
}
|
||||
priv := ed25519.NewKeyFromSeed(seed)
|
||||
pub := priv[32:]
|
||||
|
||||
pubkey := append([]byte{algEd25519}, pub...)
|
||||
hash := keyHash(name, pubkey)
|
||||
|
||||
s := &signer{
|
||||
name: name,
|
||||
hash: hash,
|
||||
sign: func(msg []byte) ([]byte, error) {
|
||||
return ed25519.Sign(priv, msg), nil
|
||||
},
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func TestSign(t *testing.T) {
|
||||
skey := "PRIVATE+KEY+PeterNeumann+c74f20a3+AYEKFALVFGyNhPJEMzD1QIDr+Y7hfZx09iUvxdXHKDFz"
|
||||
text := "If you think cryptography is the answer to your problem,\n" +
|
||||
"then you don't know what your problem is.\n"
|
||||
|
||||
signer, err := NewSigner(skey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
msg, err := Sign(&Note{Text: text}, signer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
want := `If you think cryptography is the answer to your problem,
|
||||
then you don't know what your problem is.
|
||||
|
||||
— PeterNeumann x08go/ZJkuBS9UG/SffcvIAQxVBtiFupLLr8pAcElZInNIuGUgYN1FFYC2pZSNXgKvqfqdngotpRZb6KE6RyyBwJnAM=
|
||||
`
|
||||
if string(msg) != want {
|
||||
t.Errorf("Sign: wrong output\nhave:\n%s\nwant:\n%s", msg, want)
|
||||
}
|
||||
|
||||
// Check that existing signature is replaced by new one.
|
||||
msg, err = Sign(&Note{Text: text, Sigs: []Signature{{Name: "PeterNeumann", Hash: 0xc74f20a3, Base64: "BADSIGN="}}}, signer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if string(msg) != want {
|
||||
t.Errorf("Sign replacing signature: wrong output\nhave:\n%s\nwant:\n%s", msg, want)
|
||||
}
|
||||
|
||||
// Check various bad inputs.
|
||||
_, err = Sign(&Note{Text: "abc"}, signer)
|
||||
if err == nil || err.Error() != "malformed note" {
|
||||
t.Fatalf("Sign with short text: %v, want malformed note error", err)
|
||||
}
|
||||
|
||||
_, err = Sign(&Note{Text: text, Sigs: []Signature{{Name: "a+b", Base64: "ABCD"}}})
|
||||
if err == nil || err.Error() != "malformed note" {
|
||||
t.Fatalf("Sign with bad name: %v, want malformed note error", err)
|
||||
}
|
||||
|
||||
_, err = Sign(&Note{Text: text, Sigs: []Signature{{Name: "PeterNeumann", Hash: 0xc74f20a3, Base64: "BADHASH="}}})
|
||||
if err == nil || err.Error() != "malformed note" {
|
||||
t.Fatalf("Sign with bad pre-filled signature: %v, want malformed note error", err)
|
||||
}
|
||||
|
||||
_, err = Sign(&Note{Text: text}, &badSigner{signer})
|
||||
if err == nil || err.Error() != "invalid signer" {
|
||||
t.Fatalf("Sign with bad signer: %v, want invalid signer error", err)
|
||||
}
|
||||
|
||||
_, err = Sign(&Note{Text: text}, &errSigner{signer})
|
||||
if err != errSurprise {
|
||||
t.Fatalf("Sign with failing signer: %v, want errSurprise", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVerifierList(t *testing.T) {
|
||||
peterKey := "PeterNeumann+c74f20a3+ARpc2QcUPDhMQegwxbzhKqiBfsVkmqq/LDE4izWy10TW"
|
||||
peterVerifier, err := NewVerifier(peterKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
enochKey := "EnochRoot+af0cfe78+ATtqJ7zOtqQtYqOo0CpvDXNlMhV3HeJDpjrASKGLWdop"
|
||||
enochVerifier, err := NewVerifier(enochKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
list := VerifierList(peterVerifier, enochVerifier, enochVerifier)
|
||||
v, err := list.Verifier("PeterNeumann", 0xc74f20a3)
|
||||
if v != peterVerifier || err != nil {
|
||||
t.Fatalf("list.Verifier(peter) = %v, %v, want %v, nil", v, err, peterVerifier)
|
||||
}
|
||||
v, err = list.Verifier("PeterNeumann", 0xc74f20a4)
|
||||
if v != nil || err == nil || err.Error() != "unknown key PeterNeumann+c74f20a4" {
|
||||
t.Fatalf("list.Verifier(peter bad hash) = %v, %v, want nil, unknown key error", v, err)
|
||||
}
|
||||
|
||||
v, err = list.Verifier("PeterNeuman", 0xc74f20a3)
|
||||
if v != nil || err == nil || err.Error() != "unknown key PeterNeuman+c74f20a3" {
|
||||
t.Fatalf("list.Verifier(peter bad name) = %v, %v, want nil, unknown key error", v, err)
|
||||
}
|
||||
v, err = list.Verifier("EnochRoot", 0xaf0cfe78)
|
||||
if v != nil || err == nil || err.Error() != "ambiguous key EnochRoot+af0cfe78" {
|
||||
t.Fatalf("list.Verifier(enoch) = %v, %v, want nil, ambiguous key error", v, err)
|
||||
}
|
||||
}
|
||||
|
||||
type badSigner struct {
|
||||
Signer
|
||||
}
|
||||
|
||||
func (b *badSigner) Name() string {
|
||||
return "bad name"
|
||||
}
|
||||
|
||||
var errSurprise = errors.New("surprise!")
|
||||
|
||||
type errSigner struct {
|
||||
Signer
|
||||
}
|
||||
|
||||
func (e *errSigner) Sign([]byte) ([]byte, error) {
|
||||
return nil, errSurprise
|
||||
}
|
||||
|
||||
type fixedVerifier struct{ v Verifier }
|
||||
|
||||
func (v fixedVerifier) Verifier(name string, hash uint32) (Verifier, error) {
|
||||
return v.v, nil
|
||||
}
|
||||
|
||||
func TestOpen(t *testing.T) {
|
||||
peterKey := "PeterNeumann+c74f20a3+ARpc2QcUPDhMQegwxbzhKqiBfsVkmqq/LDE4izWy10TW"
|
||||
peterVerifier, err := NewVerifier(peterKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
enochKey := "EnochRoot+af0cfe78+ATtqJ7zOtqQtYqOo0CpvDXNlMhV3HeJDpjrASKGLWdop"
|
||||
enochVerifier, err := NewVerifier(enochKey)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
text := `If you think cryptography is the answer to your problem,
|
||||
then you don't know what your problem is.
|
||||
`
|
||||
peterSig := "— PeterNeumann x08go/ZJkuBS9UG/SffcvIAQxVBtiFupLLr8pAcElZInNIuGUgYN1FFYC2pZSNXgKvqfqdngotpRZb6KE6RyyBwJnAM=\n"
|
||||
enochSig := "— EnochRoot rwz+eBzmZa0SO3NbfRGzPCpDckykFXSdeX+MNtCOXm2/5n2tiOHp+vAF1aGrQ5ovTG01oOTGwnWLox33WWd1RvMc+QQ=\n"
|
||||
|
||||
peter := Signature{"PeterNeumann", 0xc74f20a3, "x08go/ZJkuBS9UG/SffcvIAQxVBtiFupLLr8pAcElZInNIuGUgYN1FFYC2pZSNXgKvqfqdngotpRZb6KE6RyyBwJnAM="}
|
||||
enoch := Signature{"EnochRoot", 0xaf0cfe78, "rwz+eBzmZa0SO3NbfRGzPCpDckykFXSdeX+MNtCOXm2/5n2tiOHp+vAF1aGrQ5ovTG01oOTGwnWLox33WWd1RvMc+QQ="}
|
||||
|
||||
// Check one signature verified, one not.
|
||||
n, err := Open([]byte(text+"\n"+peterSig+enochSig), VerifierList(peterVerifier))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n.Text != text {
|
||||
t.Errorf("n.Text = %q, want %q", n.Text, text)
|
||||
}
|
||||
if len(n.Sigs) != 1 || n.Sigs[0] != peter {
|
||||
t.Errorf("n.Sigs:\nhave %v\nwant %v", n.Sigs, []Signature{peter})
|
||||
}
|
||||
if len(n.UnverifiedSigs) != 1 || n.UnverifiedSigs[0] != enoch {
|
||||
t.Errorf("n.UnverifiedSigs:\nhave %v\nwant %v", n.Sigs, []Signature{peter})
|
||||
}
|
||||
|
||||
// Check both verified.
|
||||
n, err = Open([]byte(text+"\n"+peterSig+enochSig), VerifierList(peterVerifier, enochVerifier))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(n.Sigs) != 2 || n.Sigs[0] != peter || n.Sigs[1] != enoch {
|
||||
t.Errorf("n.Sigs:\nhave %v\nwant %v", n.Sigs, []Signature{peter, enoch})
|
||||
}
|
||||
if len(n.UnverifiedSigs) != 0 {
|
||||
t.Errorf("n.UnverifiedSigs:\nhave %v\nwant %v", n.Sigs, []Signature{})
|
||||
}
|
||||
|
||||
// Check both unverified.
|
||||
n, err = Open([]byte(text+"\n"+peterSig+enochSig), VerifierList())
|
||||
if n != nil || err == nil {
|
||||
t.Fatalf("Open unverified = %v, %v, want nil, error", n, err)
|
||||
}
|
||||
e, ok := err.(*UnverifiedNoteError)
|
||||
if !ok {
|
||||
t.Fatalf("Open unverified: err is %T, want *UnverifiedNoteError", err)
|
||||
}
|
||||
if err.Error() != "note has no verifiable signatures" {
|
||||
t.Fatalf("Open unverified: err.Error() = %q, want %q", err.Error(), "note has no verifiable signatures")
|
||||
}
|
||||
|
||||
n = e.Note
|
||||
if n == nil {
|
||||
t.Fatalf("Open unverified: missing note in UnverifiedNoteError")
|
||||
}
|
||||
if len(n.Sigs) != 0 {
|
||||
t.Errorf("n.Sigs:\nhave %v\nwant %v", n.Sigs, []Signature{})
|
||||
}
|
||||
if len(n.UnverifiedSigs) != 2 || n.UnverifiedSigs[0] != peter || n.UnverifiedSigs[1] != enoch {
|
||||
t.Errorf("n.UnverifiedSigs:\nhave %v\nwant %v", n.Sigs, []Signature{peter, enoch})
|
||||
}
|
||||
|
||||
// Check duplicated verifier.
|
||||
_, err = Open([]byte(text+"\n"+enochSig), VerifierList(enochVerifier, peterVerifier, enochVerifier))
|
||||
if err == nil || err.Error() != "ambiguous key EnochRoot+af0cfe78" {
|
||||
t.Fatalf("Open with duplicated verifier: err=%v, want ambiguous key", err)
|
||||
}
|
||||
|
||||
// Check unused duplicated verifier.
|
||||
_, err = Open([]byte(text+"\n"+peterSig), VerifierList(enochVerifier, peterVerifier, enochVerifier))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Check too many signatures.
|
||||
n, err = Open([]byte(text+"\n"+strings.Repeat(peterSig, 101)), VerifierList(peterVerifier))
|
||||
if n != nil || err == nil || err.Error() != "malformed note" {
|
||||
t.Fatalf("Open too many verified signatures = %v, %v, want nil, malformed note error", n, err)
|
||||
}
|
||||
n, err = Open([]byte(text+"\n"+strings.Repeat(peterSig, 101)), VerifierList())
|
||||
if n != nil || err == nil || err.Error() != "malformed note" {
|
||||
t.Fatalf("Open too many verified signatures = %v, %v, want nil, malformed note error", n, err)
|
||||
}
|
||||
|
||||
// Invalid signature.
|
||||
n, err = Open([]byte(text+"\n"+peterSig[:60]+"ABCD"+peterSig[60:]), VerifierList(peterVerifier))
|
||||
if n != nil || err == nil || err.Error() != "invalid signature for key PeterNeumann+c74f20a3" {
|
||||
t.Fatalf("Open too many verified signatures = %v, %v, want nil, invalid signature error", n, err)
|
||||
}
|
||||
|
||||
// Duplicated verified and unverified signatures.
|
||||
enochABCD := Signature{"EnochRoot", 0xaf0cfe78, "rwz+eBzmZa0SO3NbfRGzPCpDckykFXSdeX+MNtCOXm2/5n" + "ABCD" + "2tiOHp+vAF1aGrQ5ovTG01oOTGwnWLox33WWd1RvMc+QQ="}
|
||||
n, err = Open([]byte(text+"\n"+peterSig+peterSig+enochSig+enochSig+enochSig[:60]+"ABCD"+enochSig[60:]), VerifierList(peterVerifier))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(n.Sigs) != 1 || n.Sigs[0] != peter {
|
||||
t.Errorf("n.Sigs:\nhave %v\nwant %v", n.Sigs, []Signature{peter})
|
||||
}
|
||||
if len(n.UnverifiedSigs) != 2 || n.UnverifiedSigs[0] != enoch || n.UnverifiedSigs[1] != enochABCD {
|
||||
t.Errorf("n.UnverifiedSigs:\nhave %v\nwant %v", n.UnverifiedSigs, []Signature{enoch, enochABCD})
|
||||
}
|
||||
|
||||
// Invalid encoded message syntax.
|
||||
badMsgs := []string{
|
||||
text,
|
||||
text + "\n",
|
||||
text + "\n" + peterSig[:len(peterSig)-1],
|
||||
"\x01" + text + "\n" + peterSig,
|
||||
"\xff" + text + "\n" + peterSig,
|
||||
text + "\n" + "— Bad Name x08go/ZJkuBS9UG/SffcvIAQxVBtiFupLLr8pAcElZInNIuGUgYN1FFYC2pZSNXgKvqfqdngotpRZb6KE6RyyBwJnAM=",
|
||||
text + "\n" + peterSig + "Unexpected line.\n",
|
||||
}
|
||||
for _, msg := range badMsgs {
|
||||
n, err := Open([]byte(msg), VerifierList(peterVerifier))
|
||||
if n != nil || err == nil || err.Error() != "malformed note" {
|
||||
t.Fatalf("Open bad msg = %v, %v, want nil, malformed note error\nmsg:\n%s", n, err, msg)
|
||||
}
|
||||
}
|
||||
|
||||
// Verifiers returns a Verifier for the wrong name or hash.
|
||||
misnamedSig := strings.Replace(peterSig, "PeterNeumann", "CarmenSandiego", -1)
|
||||
_, err = Open([]byte(text+"\n"+misnamedSig), fixedVerifier{peterVerifier})
|
||||
if err != errMismatchedVerifier {
|
||||
t.Fatalf("Open with wrong Verifier, err=%v, want errMismatchedVerifier", err)
|
||||
}
|
||||
wrongHash := strings.Replace(peterSig, "x08g", "xxxx", -1)
|
||||
_, err = Open([]byte(text+"\n"+wrongHash), fixedVerifier{peterVerifier})
|
||||
if err != errMismatchedVerifier {
|
||||
t.Fatalf("Open with wrong Verifier, err=%v, want errMismatchedVerifier", err)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkOpen(b *testing.B) {
|
||||
vkey := "PeterNeumann+c74f20a3+ARpc2QcUPDhMQegwxbzhKqiBfsVkmqq/LDE4izWy10TW"
|
||||
msg := []byte("If you think cryptography is the answer to your problem,\n" +
|
||||
"then you don't know what your problem is.\n" +
|
||||
"\n" +
|
||||
"— PeterNeumann x08go/ZJkuBS9UG/SffcvIAQxVBtiFupLLr8pAcElZInNIuGUgYN1FFYC2pZSNXgKvqfqdngotpRZb6KE6RyyBwJnAM=\n")
|
||||
|
||||
verifier, err := NewVerifier(vkey)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
verifiers := VerifierList(verifier)
|
||||
verifiers0 := VerifierList()
|
||||
|
||||
// Try with 0 signatures and 1 signature so we can tell how much each signature adds.
|
||||
|
||||
b.Run("Sig0", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, err := Open(msg, verifiers0)
|
||||
e, ok := err.(*UnverifiedNoteError)
|
||||
if !ok {
|
||||
b.Fatal("expected UnverifiedNoteError")
|
||||
}
|
||||
n := e.Note
|
||||
if len(n.Sigs) != 0 || len(n.UnverifiedSigs) != 1 {
|
||||
b.Fatal("wrong signature count")
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("Sig1", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
n, err := Open(msg, verifiers)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if len(n.Sigs) != 1 || len(n.UnverifiedSigs) != 0 {
|
||||
b.Fatal("wrong signature count")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
// Copyright 2019 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 sumdb implements the HTTP protocols for serving or accessing a module checksum database.
|
||||
package sumdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/mod/internal/lazyregexp"
|
||||
"golang.org/x/mod/module"
|
||||
"golang.org/x/mod/sumdb/tlog"
|
||||
)
|
||||
|
||||
// A ServerOps provides the external operations
|
||||
// (underlying database access and so on) needed by the [Server].
|
||||
type ServerOps interface {
|
||||
// Signed returns the signed hash of the latest tree.
|
||||
Signed(ctx context.Context) ([]byte, error)
|
||||
|
||||
// ReadRecords returns the content for the n records id through id+n-1.
|
||||
ReadRecords(ctx context.Context, id, n int64) ([][]byte, error)
|
||||
|
||||
// Lookup looks up a record for the given module,
|
||||
// returning the record ID.
|
||||
Lookup(ctx context.Context, m module.Version) (int64, error)
|
||||
|
||||
// ReadTileData reads the content of tile t.
|
||||
// It is only invoked for hash tiles (t.L ≥ 0).
|
||||
ReadTileData(ctx context.Context, t tlog.Tile) ([]byte, error)
|
||||
}
|
||||
|
||||
// A Server is the checksum database HTTP server,
|
||||
// which implements http.Handler and should be invoked
|
||||
// to serve the paths listed in [ServerPaths].
|
||||
type Server struct {
|
||||
ops ServerOps
|
||||
}
|
||||
|
||||
// NewServer returns a new Server using the given operations.
|
||||
func NewServer(ops ServerOps) *Server {
|
||||
return &Server{ops: ops}
|
||||
}
|
||||
|
||||
// ServerPaths are the URL paths the Server can (and should) serve.
|
||||
//
|
||||
// Typically a server will do:
|
||||
//
|
||||
// srv := sumdb.NewServer(ops)
|
||||
// for _, path := range sumdb.ServerPaths {
|
||||
// http.Handle(path, srv)
|
||||
// }
|
||||
var ServerPaths = []string{
|
||||
"/lookup/",
|
||||
"/latest",
|
||||
"/tile/",
|
||||
}
|
||||
|
||||
var modVerRE = lazyregexp.New(`^[^@]+@v[0-9]+\.[0-9]+\.[0-9]+(-[^@]*)?(\+incompatible)?$`)
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
switch {
|
||||
default:
|
||||
http.NotFound(w, r)
|
||||
|
||||
case strings.HasPrefix(r.URL.Path, "/lookup/"):
|
||||
mod := strings.TrimPrefix(r.URL.Path, "/lookup/")
|
||||
if !modVerRE.MatchString(mod) {
|
||||
http.Error(w, "invalid module@version syntax", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
i := strings.Index(mod, "@")
|
||||
escPath, escVers := mod[:i], mod[i+1:]
|
||||
path, err := module.UnescapePath(escPath)
|
||||
if err != nil {
|
||||
reportError(w, err)
|
||||
return
|
||||
}
|
||||
vers, err := module.UnescapeVersion(escVers)
|
||||
if err != nil {
|
||||
reportError(w, err)
|
||||
return
|
||||
}
|
||||
id, err := s.ops.Lookup(ctx, module.Version{Path: path, Version: vers})
|
||||
if err != nil {
|
||||
reportError(w, err)
|
||||
return
|
||||
}
|
||||
records, err := s.ops.ReadRecords(ctx, id, 1)
|
||||
if err != nil {
|
||||
// This should never happen - the lookup says the record exists.
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
if len(records) != 1 {
|
||||
http.Error(w, "invalid record count returned by ReadRecords", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
msg, err := tlog.FormatRecord(id, records[0])
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
signed, err := s.ops.Signed(ctx)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
|
||||
w.Write(msg)
|
||||
w.Write(signed)
|
||||
|
||||
case r.URL.Path == "/latest":
|
||||
data, err := s.ops.Signed(ctx)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
|
||||
w.Write(data)
|
||||
|
||||
case strings.HasPrefix(r.URL.Path, "/tile/"):
|
||||
t, err := tlog.ParseTilePath(r.URL.Path[1:])
|
||||
if err != nil {
|
||||
http.Error(w, "invalid tile syntax", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if t.L == -1 {
|
||||
// Record data.
|
||||
start := t.N << uint(t.H)
|
||||
records, err := s.ops.ReadRecords(ctx, start, int64(t.W))
|
||||
if err != nil {
|
||||
reportError(w, err)
|
||||
return
|
||||
}
|
||||
if len(records) != t.W {
|
||||
http.Error(w, "invalid record count returned by ReadRecords", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
var data []byte
|
||||
for i, text := range records {
|
||||
msg, err := tlog.FormatRecord(start+int64(i), text)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
data = append(data, msg...)
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain; charset=UTF-8")
|
||||
w.Write(data)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := s.ops.ReadTileData(ctx, t)
|
||||
if err != nil {
|
||||
reportError(w, err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Write(data)
|
||||
}
|
||||
}
|
||||
|
||||
// reportError reports err to w.
|
||||
// If it's a not-found, the reported error is 404.
|
||||
// Otherwise it is an internal server error.
|
||||
// The caller must only call reportError in contexts where
|
||||
// a not-found err should be reported as 404.
|
||||
func reportError(w http.ResponseWriter, err error) {
|
||||
if os.IsNotExist(err) {
|
||||
http.Error(w, err.Error(), http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
// Copyright 2019 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 storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Mem is an in-memory implementation of [Storage].
|
||||
// It is meant for tests and does not store any data to persistent storage.
|
||||
//
|
||||
// The zero value is an empty Mem ready for use.
|
||||
type Mem struct {
|
||||
mu sync.RWMutex
|
||||
table map[string]string
|
||||
}
|
||||
|
||||
// A memTx is a transaction in a Mem.
|
||||
type memTx struct {
|
||||
m *Mem
|
||||
writes []Write
|
||||
}
|
||||
|
||||
// errRetry is an internal sentinel indicating that the transaction should be retried.
|
||||
// It is never returned to the caller.
|
||||
var errRetry = errors.New("retry")
|
||||
|
||||
// ReadOnly runs f in a read-only transaction.
|
||||
func (m *Mem) ReadOnly(ctx context.Context, f func(context.Context, Transaction) error) error {
|
||||
tx := &memTx{m: m}
|
||||
for {
|
||||
err := func() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
if err := f(ctx, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
// Spurious retry with 10% probability.
|
||||
if rand.Intn(10) == 0 {
|
||||
return errRetry
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != errRetry {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReadWrite runs f in a read-write transaction.
|
||||
func (m *Mem) ReadWrite(ctx context.Context, f func(context.Context, Transaction) error) error {
|
||||
tx := &memTx{m: m}
|
||||
for {
|
||||
err := func() error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
tx.writes = []Write{}
|
||||
if err := f(ctx, tx); err != nil {
|
||||
return err
|
||||
}
|
||||
// Spurious retry with 10% probability.
|
||||
if rand.Intn(10) == 0 {
|
||||
return errRetry
|
||||
}
|
||||
if m.table == nil {
|
||||
m.table = make(map[string]string)
|
||||
}
|
||||
for _, w := range tx.writes {
|
||||
if w.Value == "" {
|
||||
delete(m.table, w.Key)
|
||||
} else {
|
||||
m.table[w.Key] = w.Value
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != errRetry {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReadValues returns the values associated with the given keys.
|
||||
func (tx *memTx) ReadValues(ctx context.Context, keys []string) ([]string, error) {
|
||||
vals := make([]string, len(keys))
|
||||
for i, key := range keys {
|
||||
vals[i] = tx.m.table[key]
|
||||
}
|
||||
return vals, nil
|
||||
}
|
||||
|
||||
// ReadValue returns the value associated with the single key.
|
||||
func (tx *memTx) ReadValue(ctx context.Context, key string) (string, error) {
|
||||
return tx.m.table[key], nil
|
||||
}
|
||||
|
||||
// BufferWrites buffers a list of writes to be applied
|
||||
// to the table when the transaction commits.
|
||||
// The changes are not visible to reads within the transaction.
|
||||
// The map argument is not used after the call returns.
|
||||
func (tx *memTx) BufferWrites(list []Write) error {
|
||||
if tx.writes == nil {
|
||||
panic("BufferWrite on read-only transaction")
|
||||
}
|
||||
tx.writes = append(tx.writes, list...)
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// Copyright 2019 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 storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMem(t *testing.T) {
|
||||
TestStorage(t, context.Background(), new(Mem))
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2019 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 storage defines storage interfaces for and a basic implementation of a checksum database.
|
||||
package storage
|
||||
|
||||
import "context"
|
||||
|
||||
// A Storage is a transaction key-value storage system.
|
||||
type Storage interface {
|
||||
// ReadOnly runs f in a read-only transaction.
|
||||
// It is equivalent to ReadWrite except that the
|
||||
// transaction's BufferWrite method will fail unconditionally.
|
||||
// (The implementation may be able to optimize the
|
||||
// transaction if it knows at the start that no writes will happen.)
|
||||
ReadOnly(ctx context.Context, f func(context.Context, Transaction) error) error
|
||||
|
||||
// ReadWrite runs f in a read-write transaction.
|
||||
// If f returns an error, the transaction aborts and returns that error.
|
||||
// If f returns nil, the transaction attempts to commit and then return nil.
|
||||
// Otherwise it tries again. Note that f may be called multiple times and that
|
||||
// the result only describes the effect of the final call to f.
|
||||
// The caller must take care not to use any state computed during
|
||||
// earlier calls to f, or even the last call to f when an error is returned.
|
||||
ReadWrite(ctx context.Context, f func(context.Context, Transaction) error) error
|
||||
}
|
||||
|
||||
// A Transaction provides read and write operations within a transaction,
|
||||
// as executed by [Storage]'s ReadOnly or ReadWrite methods.
|
||||
type Transaction interface {
|
||||
// ReadValue reads the value associated with a single key.
|
||||
// If there is no value associated with that key, ReadKey returns an empty value.
|
||||
// An error is only returned for problems accessing the storage.
|
||||
ReadValue(ctx context.Context, key string) (value string, err error)
|
||||
|
||||
// ReadValues reads the values associated with the given keys.
|
||||
// If there is no value stored for a given key, ReadValues returns an empty value for that key.
|
||||
// An error is only returned for problems accessing the storage.
|
||||
ReadValues(ctx context.Context, keys []string) (values []string, err error)
|
||||
|
||||
// BufferWrites buffers the given writes,
|
||||
// to be applied at the end of the transaction.
|
||||
// BufferWrites panics if this is a ReadOnly transaction.
|
||||
// It returns an error if it detects any other problems.
|
||||
// The behavior of multiple writes buffered using the same key
|
||||
// is undefined: it may return an error or not.
|
||||
BufferWrites(writes []Write) error
|
||||
}
|
||||
|
||||
// A Write is a single change to be applied at the end of a read-write transaction.
|
||||
// A Write with an empty value deletes the value associated with the given key.
|
||||
type Write struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright 2019 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 storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestStorage tests a Storage implementation.
|
||||
func TestStorage(t *testing.T, ctx context.Context, storage Storage) {
|
||||
s := storage
|
||||
|
||||
// Insert records.
|
||||
err := s.ReadWrite(ctx, func(ctx context.Context, tx Transaction) error {
|
||||
for i := 0; i < 10; i++ {
|
||||
err := tx.BufferWrites([]Write{
|
||||
{Key: fmt.Sprint(i), Value: fmt.Sprint(-i)},
|
||||
{Key: fmt.Sprint(1000 + i), Value: fmt.Sprint(-1000 - i)},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Read the records back.
|
||||
testRead := func() {
|
||||
err := s.ReadOnly(ctx, func(ctx context.Context, tx Transaction) error {
|
||||
for i := int64(0); i < 1010; i++ {
|
||||
if i == 10 {
|
||||
i = 1000
|
||||
}
|
||||
val, err := tx.ReadValue(ctx, fmt.Sprint(i))
|
||||
if err != nil {
|
||||
t.Fatalf("reading %v: %v", i, err)
|
||||
}
|
||||
if want := fmt.Sprint(-i); val != want {
|
||||
t.Fatalf("ReadValue %v = %q, want %v", i, val, want)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
testRead()
|
||||
|
||||
// Buffered writes in failed transaction should not be applied.
|
||||
err = s.ReadWrite(ctx, func(ctx context.Context, tx Transaction) error {
|
||||
tx.BufferWrites([]Write{
|
||||
{Key: fmt.Sprint(0), Value: ""}, // delete
|
||||
{Key: fmt.Sprint(1), Value: "overwrite"}, // overwrite
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return io.ErrUnexpectedEOF
|
||||
})
|
||||
if err != io.ErrUnexpectedEOF {
|
||||
t.Fatalf("ReadWrite returned %v, want ErrUnexpectedEOF", err)
|
||||
}
|
||||
|
||||
// All same values should still be there.
|
||||
testRead()
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright 2019 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 sumdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"golang.org/x/mod/module"
|
||||
"golang.org/x/mod/sumdb/note"
|
||||
"golang.org/x/mod/sumdb/tlog"
|
||||
)
|
||||
|
||||
// NewTestServer constructs a new [TestServer]
|
||||
// that will sign its tree with the given signer key
|
||||
// (see [golang.org/x/mod/sumdb/note])
|
||||
// and fetch new records as needed by calling gosum.
|
||||
func NewTestServer(signer string, gosum func(path, vers string) ([]byte, error)) *TestServer {
|
||||
return &TestServer{signer: signer, gosum: gosum}
|
||||
}
|
||||
|
||||
// A TestServer is an in-memory implementation of [ServerOps] for testing.
|
||||
type TestServer struct {
|
||||
signer string
|
||||
gosum func(path, vers string) ([]byte, error)
|
||||
|
||||
mu sync.Mutex
|
||||
hashes testHashes
|
||||
records [][]byte
|
||||
lookup map[string]int64
|
||||
}
|
||||
|
||||
// testHashes implements tlog.HashReader, reading from a slice.
|
||||
type testHashes []tlog.Hash
|
||||
|
||||
func (h testHashes) ReadHashes(indexes []int64) ([]tlog.Hash, error) {
|
||||
var list []tlog.Hash
|
||||
for _, id := range indexes {
|
||||
list = append(list, h[id])
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (s *TestServer) Signed(ctx context.Context) ([]byte, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
size := int64(len(s.records))
|
||||
h, err := tlog.TreeHash(size, s.hashes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
text := tlog.FormatTree(tlog.Tree{N: size, Hash: h})
|
||||
signer, err := note.NewSigner(s.signer)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return note.Sign(¬e.Note{Text: string(text)}, signer)
|
||||
}
|
||||
|
||||
func (s *TestServer) ReadRecords(ctx context.Context, id, n int64) ([][]byte, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
var list [][]byte
|
||||
for i := int64(0); i < n; i++ {
|
||||
if id+i >= int64(len(s.records)) {
|
||||
return nil, fmt.Errorf("missing records")
|
||||
}
|
||||
list = append(list, s.records[id+i])
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func (s *TestServer) Lookup(ctx context.Context, m module.Version) (int64, error) {
|
||||
key := m.String()
|
||||
s.mu.Lock()
|
||||
id, ok := s.lookup[key]
|
||||
s.mu.Unlock()
|
||||
if ok {
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Look up module and compute go.sum lines.
|
||||
data, err := s.gosum(m.Path, m.Version)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
// We ran the fetch without the lock.
|
||||
// If another fetch happened and committed, use it instead.
|
||||
id, ok = s.lookup[key]
|
||||
if ok {
|
||||
return id, nil
|
||||
}
|
||||
|
||||
// Add record.
|
||||
id = int64(len(s.records))
|
||||
s.records = append(s.records, data)
|
||||
if s.lookup == nil {
|
||||
s.lookup = make(map[string]int64)
|
||||
}
|
||||
s.lookup[key] = id
|
||||
hashes, err := tlog.StoredHashesForRecordHash(id, tlog.RecordHash(data), s.hashes)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
s.hashes = append(s.hashes, hashes...)
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (s *TestServer) ReadTileData(ctx context.Context, t tlog.Tile) ([]byte, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
return tlog.ReadTileData(t, s.hashes)
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
// Copyright 2019 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 tlog
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCertificateTransparency(t *testing.T) {
|
||||
// Test that we can verify actual Certificate Transparency proofs.
|
||||
// (The other tests check that we can verify our own proofs;
|
||||
// this is a test that the two are compatible.)
|
||||
|
||||
if testing.Short() {
|
||||
t.Skip("skipping in -short mode")
|
||||
}
|
||||
|
||||
var root ctTree
|
||||
httpGET(t, "http://ct.googleapis.com/logs/argon2020/ct/v1/get-sth", &root)
|
||||
|
||||
var leaf ctEntries
|
||||
httpGET(t, "http://ct.googleapis.com/logs/argon2020/ct/v1/get-entries?start=10000&end=10000", &leaf)
|
||||
hash := RecordHash(leaf.Entries[0].Data)
|
||||
|
||||
var rp ctRecordProof
|
||||
httpGET(t, "http://ct.googleapis.com/logs/argon2020/ct/v1/get-proof-by-hash?tree_size="+fmt.Sprint(root.Size)+"&hash="+url.QueryEscape(hash.String()), &rp)
|
||||
|
||||
err := CheckRecord(rp.Proof, root.Size, root.Hash, 10000, hash)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var tp ctTreeProof
|
||||
httpGET(t, "http://ct.googleapis.com/logs/argon2020/ct/v1/get-sth-consistency?first=3654490&second="+fmt.Sprint(root.Size), &tp)
|
||||
|
||||
oh, _ := ParseHash("AuIZ5V6sDUj1vn3Y1K85oOaQ7y+FJJKtyRTl1edIKBQ=")
|
||||
err = CheckTree(tp.Proof, root.Size, root.Hash, 3654490, oh)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
type ctTree struct {
|
||||
Size int64 `json:"tree_size"`
|
||||
Hash Hash `json:"sha256_root_hash"`
|
||||
}
|
||||
|
||||
type ctEntries struct {
|
||||
Entries []*ctEntry
|
||||
}
|
||||
|
||||
type ctEntry struct {
|
||||
Data []byte `json:"leaf_input"`
|
||||
}
|
||||
|
||||
type ctRecordProof struct {
|
||||
Index int64 `json:"leaf_index"`
|
||||
Proof RecordProof `json:"audit_path"`
|
||||
}
|
||||
|
||||
type ctTreeProof struct {
|
||||
Proof TreeProof `json:"consistency"`
|
||||
}
|
||||
|
||||
func httpGET(t *testing.T, url string, targ interface{}) {
|
||||
if testing.Verbose() {
|
||||
println()
|
||||
println(url)
|
||||
}
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if testing.Verbose() {
|
||||
os.Stdout.Write(data)
|
||||
}
|
||||
err = json.Unmarshal(data, targ)
|
||||
if err != nil {
|
||||
println(url)
|
||||
os.Stdout.Write(data)
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// Copyright 2019 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 tlog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
// A Tree is a tree description, to be signed by a go.sum database server.
|
||||
type Tree struct {
|
||||
N int64
|
||||
Hash Hash
|
||||
}
|
||||
|
||||
// FormatTree formats a tree description for inclusion in a note.
|
||||
//
|
||||
// The encoded form is three lines, each ending in a newline (U+000A):
|
||||
//
|
||||
// go.sum database tree
|
||||
// N
|
||||
// Hash
|
||||
//
|
||||
// where N is in decimal and Hash is in base64.
|
||||
//
|
||||
// A future backwards-compatible encoding may add additional lines,
|
||||
// which the parser can ignore.
|
||||
// A future backwards-incompatible encoding would use a different
|
||||
// first line (for example, "go.sum database tree v2").
|
||||
func FormatTree(tree Tree) []byte {
|
||||
return []byte(fmt.Sprintf("go.sum database tree\n%d\n%s\n", tree.N, tree.Hash))
|
||||
}
|
||||
|
||||
var errMalformedTree = errors.New("malformed tree note")
|
||||
var treePrefix = []byte("go.sum database tree\n")
|
||||
|
||||
// ParseTree parses a formatted tree root description.
|
||||
func ParseTree(text []byte) (tree Tree, err error) {
|
||||
// The message looks like:
|
||||
//
|
||||
// go.sum database tree
|
||||
// 2
|
||||
// nND/nri/U0xuHUrYSy0HtMeal2vzD9V4k/BO79C+QeI=
|
||||
//
|
||||
// For forwards compatibility, extra text lines after the encoding are ignored.
|
||||
if !bytes.HasPrefix(text, treePrefix) || bytes.Count(text, []byte("\n")) < 3 || len(text) > 1e6 {
|
||||
return Tree{}, errMalformedTree
|
||||
}
|
||||
|
||||
lines := strings.SplitN(string(text), "\n", 4)
|
||||
n, err := strconv.ParseInt(lines[1], 10, 64)
|
||||
if err != nil || n < 0 || lines[1] != strconv.FormatInt(n, 10) {
|
||||
return Tree{}, errMalformedTree
|
||||
}
|
||||
|
||||
h, err := base64.StdEncoding.DecodeString(lines[2])
|
||||
if err != nil || len(h) != HashSize {
|
||||
return Tree{}, errMalformedTree
|
||||
}
|
||||
|
||||
var hash Hash
|
||||
copy(hash[:], h)
|
||||
return Tree{n, hash}, nil
|
||||
}
|
||||
|
||||
var errMalformedRecord = errors.New("malformed record data")
|
||||
|
||||
// FormatRecord formats a record for serving to a client
|
||||
// in a lookup response or data tile.
|
||||
//
|
||||
// The encoded form is the record ID as a single number,
|
||||
// then the text of the record, and then a terminating blank line.
|
||||
// Record text must be valid UTF-8 and must not contain any ASCII control
|
||||
// characters (those below U+0020) other than newline (U+000A).
|
||||
// It must end in a terminating newline and not contain any blank lines.
|
||||
func FormatRecord(id int64, text []byte) (msg []byte, err error) {
|
||||
if !isValidRecordText(text) {
|
||||
return nil, errMalformedRecord
|
||||
}
|
||||
msg = []byte(fmt.Sprintf("%d\n", id))
|
||||
msg = append(msg, text...)
|
||||
msg = append(msg, '\n')
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
// isValidRecordText reports whether text is syntactically valid record text.
|
||||
func isValidRecordText(text []byte) bool {
|
||||
var last rune
|
||||
for i := 0; i < len(text); {
|
||||
r, size := utf8.DecodeRune(text[i:])
|
||||
if r < 0x20 && r != '\n' || r == utf8.RuneError && size == 1 || last == '\n' && r == '\n' {
|
||||
return false
|
||||
}
|
||||
i += size
|
||||
last = r
|
||||
}
|
||||
if last != '\n' {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ParseRecord parses a record description at the start of text,
|
||||
// stopping immediately after the terminating blank line.
|
||||
// It returns the record id, the record text, and the remainder of text.
|
||||
func ParseRecord(msg []byte) (id int64, text, rest []byte, err error) {
|
||||
// Leading record id.
|
||||
i := bytes.IndexByte(msg, '\n')
|
||||
if i < 0 {
|
||||
return 0, nil, nil, errMalformedRecord
|
||||
}
|
||||
id, err = strconv.ParseInt(string(msg[:i]), 10, 64)
|
||||
if err != nil {
|
||||
return 0, nil, nil, errMalformedRecord
|
||||
}
|
||||
msg = msg[i+1:]
|
||||
|
||||
// Record text.
|
||||
i = bytes.Index(msg, []byte("\n\n"))
|
||||
if i < 0 {
|
||||
return 0, nil, nil, errMalformedRecord
|
||||
}
|
||||
text, rest = msg[:i+1], msg[i+2:]
|
||||
if !isValidRecordText(text) {
|
||||
return 0, nil, nil, errMalformedRecord
|
||||
}
|
||||
return id, text, rest, nil
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
// Copyright 2019 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 tlog
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatTree(t *testing.T) {
|
||||
n := int64(123456789012)
|
||||
h := RecordHash([]byte("hello world"))
|
||||
golden := "go.sum database tree\n123456789012\nTszzRgjTG6xce+z2AG31kAXYKBgQVtCSCE40HmuwBb0=\n"
|
||||
b := FormatTree(Tree{n, h})
|
||||
if string(b) != golden {
|
||||
t.Errorf("FormatTree(...) = %q, want %q", b, golden)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTree(t *testing.T) {
|
||||
in := "go.sum database tree\n123456789012\nTszzRgjTG6xce+z2AG31kAXYKBgQVtCSCE40HmuwBb0=\n"
|
||||
goldH := RecordHash([]byte("hello world"))
|
||||
goldN := int64(123456789012)
|
||||
tree, err := ParseTree([]byte(in))
|
||||
if tree.N != goldN || tree.Hash != goldH || err != nil {
|
||||
t.Fatalf("ParseTree(...) = Tree{%d, %v}, %v, want Tree{%d, %v}, nil", tree.N, tree.Hash, err, goldN, goldH)
|
||||
}
|
||||
|
||||
// Check invalid trees.
|
||||
var badTrees = []string{
|
||||
"not-" + in,
|
||||
"go.sum database tree\n0xabcdef\nTszzRgjTG6xce+z2AG31kAXYKBgQVtCSCE40HmuwBb0=\n",
|
||||
"go.sum database tree\n123456789012\nTszzRgjTG6xce+z2AG31kAXYKBgQVtCSCE40HmuwBTOOBIG=\n",
|
||||
}
|
||||
for _, bad := range badTrees {
|
||||
_, err := ParseTree([]byte(bad))
|
||||
if err == nil {
|
||||
t.Fatalf("ParseTree(%q) succeeded, want failure", in)
|
||||
}
|
||||
}
|
||||
|
||||
// Check junk on end is ignored.
|
||||
var goodTrees = []string{
|
||||
in + "JOE",
|
||||
in + "JOE\n",
|
||||
in + strings.Repeat("JOE\n", 1000),
|
||||
}
|
||||
for _, good := range goodTrees {
|
||||
_, err := ParseTree([]byte(good))
|
||||
if tree.N != goldN || tree.Hash != goldH || err != nil {
|
||||
t.Fatalf("ParseTree(...+%q) = Tree{%d, %v}, %v, want Tree{%d, %v}, nil", good[len(in):], tree.N, tree.Hash, err, goldN, goldH)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatRecord(t *testing.T) {
|
||||
id := int64(123456789012)
|
||||
text := "hello, world\n"
|
||||
golden := "123456789012\nhello, world\n\n"
|
||||
msg, err := FormatRecord(id, []byte(text))
|
||||
if err != nil {
|
||||
t.Fatalf("FormatRecord: %v", err)
|
||||
}
|
||||
if string(msg) != golden {
|
||||
t.Fatalf("FormatRecord(...) = %q, want %q", msg, golden)
|
||||
}
|
||||
|
||||
var badTexts = []string{
|
||||
"",
|
||||
"hello\nworld",
|
||||
"hello\n\nworld\n",
|
||||
"hello\x01world\n",
|
||||
}
|
||||
for _, bad := range badTexts {
|
||||
msg, err := FormatRecord(id, []byte(bad))
|
||||
if err == nil {
|
||||
t.Errorf("FormatRecord(id, %q) = %q, want error", bad, msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRecord(t *testing.T) {
|
||||
in := "123456789012\nhello, world\n\njunk on end\x01\xff"
|
||||
goldID := int64(123456789012)
|
||||
goldText := "hello, world\n"
|
||||
goldRest := "junk on end\x01\xff"
|
||||
id, text, rest, err := ParseRecord([]byte(in))
|
||||
if id != goldID || string(text) != goldText || string(rest) != goldRest || err != nil {
|
||||
t.Fatalf("ParseRecord(%q) = %d, %q, %q, %v, want %d, %q, %q, nil", in, id, text, rest, err, goldID, goldText, goldRest)
|
||||
}
|
||||
|
||||
in = "123456789012\nhello, world\n\n"
|
||||
id, text, rest, err = ParseRecord([]byte(in))
|
||||
if id != goldID || string(text) != goldText || len(rest) != 0 || err != nil {
|
||||
t.Fatalf("ParseRecord(%q) = %d, %q, %q, %v, want %d, %q, %q, nil", in, id, text, rest, err, goldID, goldText, "")
|
||||
}
|
||||
if rest == nil {
|
||||
t.Fatalf("ParseRecord(%q): rest = []byte(nil), want []byte{}", in)
|
||||
}
|
||||
|
||||
// Check invalid records.
|
||||
var badRecords = []string{
|
||||
"not-" + in,
|
||||
"123\nhello\x01world\n\n",
|
||||
"123\nhello\xffworld\n\n",
|
||||
"123\nhello world\n",
|
||||
"0x123\nhello world\n\n",
|
||||
}
|
||||
for _, bad := range badRecords {
|
||||
id, text, rest, err := ParseRecord([]byte(bad))
|
||||
if err == nil {
|
||||
t.Fatalf("ParseRecord(%q) = %d, %q, %q, nil, want error", in, id, text, rest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// FuzzParseTree tests that ParseTree never crashes
|
||||
func FuzzParseTree(f *testing.F) {
|
||||
f.Add([]byte("go.sum database tree\n123456789012\nTszzRgjTG6xce+z2AG31kAXYKBgQVtCSCE40HmuwBb0=\n"))
|
||||
f.Fuzz(func(t *testing.T, text []byte) {
|
||||
ParseTree(text)
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzParseRecord tests that ParseRecord never crashes
|
||||
func FuzzParseRecord(f *testing.F) {
|
||||
f.Add([]byte("12345\nhello\n\n"))
|
||||
f.Fuzz(func(t *testing.T, msg []byte) {
|
||||
ParseRecord(msg)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,435 @@
|
||||
// Copyright 2019 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 tlog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// A Tile is a description of a transparency log tile.
|
||||
// A tile of height H at level L offset N lists W consecutive hashes
|
||||
// at level H*L of the tree starting at offset N*(2**H).
|
||||
// A complete tile lists 2**H hashes; a partial tile lists fewer.
|
||||
// Note that a tile represents the entire subtree of height H
|
||||
// with those hashes as the leaves. The levels above H*L
|
||||
// can be reconstructed by hashing the leaves.
|
||||
//
|
||||
// Each Tile can be encoded as a “tile coordinate path”
|
||||
// of the form tile/H/L/NNN[.p/W].
|
||||
// The .p/W suffix is present only for partial tiles, meaning W < 2**H.
|
||||
// The NNN element is an encoding of N into 3-digit path elements.
|
||||
// All but the last path element begins with an "x".
|
||||
// For example,
|
||||
// Tile{H: 3, L: 4, N: 1234067, W: 1}'s path
|
||||
// is tile/3/4/x001/x234/067.p/1, and
|
||||
// Tile{H: 3, L: 4, N: 1234067, W: 8}'s path
|
||||
// is tile/3/4/x001/x234/067.
|
||||
// See the [Tile.Path] method and the [ParseTilePath] function.
|
||||
//
|
||||
// The special level L=-1 holds raw record data instead of hashes.
|
||||
// In this case, the level encodes into a tile path as the path element
|
||||
// "data" instead of "-1".
|
||||
//
|
||||
// See also https://golang.org/design/25530-sumdb#checksum-database
|
||||
// and https://research.swtch.com/tlog#tiling_a_log.
|
||||
type Tile struct {
|
||||
H int // height of tile (1 ≤ H ≤ 30)
|
||||
L int // level in tiling (-1 ≤ L ≤ 63)
|
||||
N int64 // number within level (0 ≤ N, unbounded)
|
||||
W int // width of tile (1 ≤ W ≤ 2**H; 2**H is complete tile)
|
||||
}
|
||||
|
||||
// TileForIndex returns the tile of fixed height h ≥ 1
|
||||
// and least width storing the given hash storage index.
|
||||
//
|
||||
// If h ≤ 0, [TileForIndex] panics.
|
||||
func TileForIndex(h int, index int64) Tile {
|
||||
if h <= 0 {
|
||||
panic(fmt.Sprintf("TileForIndex: invalid height %d", h))
|
||||
}
|
||||
t, _, _ := tileForIndex(h, index)
|
||||
return t
|
||||
}
|
||||
|
||||
// tileForIndex returns the tile of height h ≥ 1
|
||||
// storing the given hash index, which can be
|
||||
// reconstructed using tileHash(data[start:end]).
|
||||
func tileForIndex(h int, index int64) (t Tile, start, end int) {
|
||||
level, n := SplitStoredHashIndex(index)
|
||||
t.H = h
|
||||
t.L = level / h
|
||||
level -= t.L * h // now level within tile
|
||||
t.N = n << uint(level) >> uint(t.H)
|
||||
n -= t.N << uint(t.H) >> uint(level) // now n within tile at level
|
||||
t.W = int((n + 1) << uint(level))
|
||||
return t, int(n<<uint(level)) * HashSize, int((n+1)<<uint(level)) * HashSize
|
||||
}
|
||||
|
||||
// HashFromTile returns the hash at the given storage index,
|
||||
// provided that t == TileForIndex(t.H, index) or a wider version,
|
||||
// and data is t's tile data (of length at least t.W*HashSize).
|
||||
func HashFromTile(t Tile, data []byte, index int64) (Hash, error) {
|
||||
if t.H < 1 || t.H > 30 || t.L < 0 || t.L >= 64 || t.W < 1 || t.W > 1<<uint(t.H) {
|
||||
return Hash{}, fmt.Errorf("invalid tile %v", t.Path())
|
||||
}
|
||||
if len(data) < t.W*HashSize {
|
||||
return Hash{}, fmt.Errorf("data len %d too short for tile %v", len(data), t.Path())
|
||||
}
|
||||
t1, start, end := tileForIndex(t.H, index)
|
||||
if t.L != t1.L || t.N != t1.N || t.W < t1.W {
|
||||
return Hash{}, fmt.Errorf("index %v is in %v not %v", index, t1.Path(), t.Path())
|
||||
}
|
||||
return tileHash(data[start:end]), nil
|
||||
}
|
||||
|
||||
// tileHash computes the subtree hash corresponding to the (2^K)-1 hashes in data.
|
||||
func tileHash(data []byte) Hash {
|
||||
if len(data) == 0 {
|
||||
panic("bad math in tileHash")
|
||||
}
|
||||
if len(data) == HashSize {
|
||||
var h Hash
|
||||
copy(h[:], data)
|
||||
return h
|
||||
}
|
||||
n := len(data) / 2
|
||||
return NodeHash(tileHash(data[:n]), tileHash(data[n:]))
|
||||
}
|
||||
|
||||
// NewTiles returns the coordinates of the tiles of height h ≥ 1
|
||||
// that must be published when publishing from a tree of
|
||||
// size newTreeSize to replace a tree of size oldTreeSize.
|
||||
// (No tiles need to be published for a tree of size zero.)
|
||||
//
|
||||
// If h ≤ 0, NewTiles panics.
|
||||
func NewTiles(h int, oldTreeSize, newTreeSize int64) []Tile {
|
||||
if h <= 0 {
|
||||
panic(fmt.Sprintf("NewTiles: invalid height %d", h))
|
||||
}
|
||||
H := uint(h)
|
||||
var tiles []Tile
|
||||
for level := uint(0); newTreeSize>>(H*level) > 0; level++ {
|
||||
oldN := oldTreeSize >> (H * level)
|
||||
newN := newTreeSize >> (H * level)
|
||||
for n := oldN >> H; n < newN>>H; n++ {
|
||||
tiles = append(tiles, Tile{H: h, L: int(level), N: n, W: 1 << H})
|
||||
}
|
||||
n := newN >> H
|
||||
maxW := int(newN - n<<H)
|
||||
minW := 1
|
||||
if oldN > n<<H {
|
||||
minW = int(oldN - n<<H)
|
||||
}
|
||||
for w := minW; w <= maxW; w++ {
|
||||
tiles = append(tiles, Tile{H: h, L: int(level), N: n, W: w})
|
||||
}
|
||||
}
|
||||
return tiles
|
||||
}
|
||||
|
||||
// ReadTileData reads the hashes for tile t from r
|
||||
// and returns the corresponding tile data.
|
||||
func ReadTileData(t Tile, r HashReader) ([]byte, error) {
|
||||
size := t.W
|
||||
if size == 0 {
|
||||
size = 1 << uint(t.H)
|
||||
}
|
||||
start := t.N << uint(t.H)
|
||||
indexes := make([]int64, size)
|
||||
for i := 0; i < size; i++ {
|
||||
indexes[i] = StoredHashIndex(t.H*t.L, start+int64(i))
|
||||
}
|
||||
|
||||
hashes, err := r.ReadHashes(indexes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(hashes) != len(indexes) {
|
||||
return nil, fmt.Errorf("tlog: ReadHashes(%d indexes) = %d hashes", len(indexes), len(hashes))
|
||||
}
|
||||
|
||||
tile := make([]byte, size*HashSize)
|
||||
for i := 0; i < size; i++ {
|
||||
copy(tile[i*HashSize:], hashes[i][:])
|
||||
}
|
||||
return tile, nil
|
||||
}
|
||||
|
||||
// To limit the size of any particular directory listing,
|
||||
// we encode the (possibly very large) number N
|
||||
// by encoding three digits at a time.
|
||||
// For example, 123456789 encodes as x123/x456/789.
|
||||
// Each directory has at most 1000 each xNNN, NNN, and NNN.p children,
|
||||
// so there are at most 3000 entries in any one directory.
|
||||
const pathBase = 1000
|
||||
|
||||
// Path returns a tile coordinate path describing t.
|
||||
func (t Tile) Path() string {
|
||||
n := t.N
|
||||
nStr := fmt.Sprintf("%03d", n%pathBase)
|
||||
for n >= pathBase {
|
||||
n /= pathBase
|
||||
nStr = fmt.Sprintf("x%03d/%s", n%pathBase, nStr)
|
||||
}
|
||||
pStr := ""
|
||||
if t.W != 1<<uint(t.H) {
|
||||
pStr = fmt.Sprintf(".p/%d", t.W)
|
||||
}
|
||||
var L string
|
||||
if t.L == -1 {
|
||||
L = "data"
|
||||
} else {
|
||||
L = fmt.Sprintf("%d", t.L)
|
||||
}
|
||||
return fmt.Sprintf("tile/%d/%s/%s%s", t.H, L, nStr, pStr)
|
||||
}
|
||||
|
||||
// ParseTilePath parses a tile coordinate path.
|
||||
func ParseTilePath(path string) (Tile, error) {
|
||||
f := strings.Split(path, "/")
|
||||
if len(f) < 4 || f[0] != "tile" {
|
||||
return Tile{}, &badPathError{path}
|
||||
}
|
||||
h, err1 := strconv.Atoi(f[1])
|
||||
isData := false
|
||||
if f[2] == "data" {
|
||||
isData = true
|
||||
f[2] = "0"
|
||||
}
|
||||
l, err2 := strconv.Atoi(f[2])
|
||||
if err1 != nil || err2 != nil || h < 1 || l < 0 || h > 30 {
|
||||
return Tile{}, &badPathError{path}
|
||||
}
|
||||
w := 1 << uint(h)
|
||||
if dotP := f[len(f)-2]; strings.HasSuffix(dotP, ".p") {
|
||||
ww, err := strconv.Atoi(f[len(f)-1])
|
||||
if err != nil || ww <= 0 || ww >= w {
|
||||
return Tile{}, &badPathError{path}
|
||||
}
|
||||
w = ww
|
||||
f[len(f)-2] = dotP[:len(dotP)-len(".p")]
|
||||
f = f[:len(f)-1]
|
||||
}
|
||||
f = f[3:]
|
||||
n := int64(0)
|
||||
for _, s := range f {
|
||||
nn, err := strconv.Atoi(strings.TrimPrefix(s, "x"))
|
||||
if err != nil || nn < 0 || nn >= pathBase {
|
||||
return Tile{}, &badPathError{path}
|
||||
}
|
||||
n = n*pathBase + int64(nn)
|
||||
}
|
||||
if isData {
|
||||
l = -1
|
||||
}
|
||||
t := Tile{H: h, L: l, N: n, W: w}
|
||||
if path != t.Path() {
|
||||
return Tile{}, &badPathError{path}
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
type badPathError struct {
|
||||
path string
|
||||
}
|
||||
|
||||
func (e *badPathError) Error() string {
|
||||
return fmt.Sprintf("malformed tile path %q", e.path)
|
||||
}
|
||||
|
||||
// A TileReader reads tiles from a go.sum database log.
|
||||
type TileReader interface {
|
||||
// Height returns the height of the available tiles.
|
||||
Height() int
|
||||
|
||||
// ReadTiles returns the data for each requested tile.
|
||||
// If ReadTiles returns err == nil, it must also return
|
||||
// a data record for each tile (len(data) == len(tiles))
|
||||
// and each data record must be the correct length
|
||||
// (len(data[i]) == tiles[i].W*HashSize).
|
||||
//
|
||||
// An implementation of ReadTiles typically reads
|
||||
// them from an on-disk cache or else from a remote
|
||||
// tile server. Tile data downloaded from a server should
|
||||
// be considered suspect and not saved into a persistent
|
||||
// on-disk cache before returning from ReadTiles.
|
||||
// When the client confirms the validity of the tile data,
|
||||
// it will call SaveTiles to signal that they can be safely
|
||||
// written to persistent storage.
|
||||
// See also https://research.swtch.com/tlog#authenticating_tiles.
|
||||
ReadTiles(tiles []Tile) (data [][]byte, err error)
|
||||
|
||||
// SaveTiles informs the TileReader that the tile data
|
||||
// returned by ReadTiles has been confirmed as valid
|
||||
// and can be saved in persistent storage (on disk).
|
||||
SaveTiles(tiles []Tile, data [][]byte)
|
||||
}
|
||||
|
||||
// TileHashReader returns a HashReader that satisfies requests
|
||||
// by loading tiles of the given tree.
|
||||
//
|
||||
// The returned [HashReader] checks that loaded tiles are
|
||||
// valid for the given tree. Therefore, any hashes returned
|
||||
// by the HashReader are already proven to be in the tree.
|
||||
func TileHashReader(tree Tree, tr TileReader) HashReader {
|
||||
return &tileHashReader{tree: tree, tr: tr}
|
||||
}
|
||||
|
||||
type tileHashReader struct {
|
||||
tree Tree
|
||||
tr TileReader
|
||||
}
|
||||
|
||||
// tileParent returns t's k'th tile parent in the tiles for a tree of size n.
|
||||
// If there is no such parent, tileParent returns Tile{}.
|
||||
func tileParent(t Tile, k int, n int64) Tile {
|
||||
t.L += k
|
||||
t.N >>= uint(k * t.H)
|
||||
t.W = 1 << uint(t.H)
|
||||
if max := n >> uint(t.L*t.H); t.N<<uint(t.H)+int64(t.W) >= max {
|
||||
if t.N<<uint(t.H) >= max {
|
||||
return Tile{}
|
||||
}
|
||||
t.W = int(max - t.N<<uint(t.H))
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
||||
func (r *tileHashReader) ReadHashes(indexes []int64) ([]Hash, error) {
|
||||
h := r.tr.Height()
|
||||
|
||||
tileOrder := make(map[Tile]int) // tileOrder[tileKey(tiles[i])] = i
|
||||
var tiles []Tile
|
||||
|
||||
// Plan to fetch tiles necessary to recompute tree hash.
|
||||
// If it matches, those tiles are authenticated.
|
||||
stx := subTreeIndex(0, r.tree.N, nil)
|
||||
stxTileOrder := make([]int, len(stx))
|
||||
for i, x := range stx {
|
||||
tile, _, _ := tileForIndex(h, x)
|
||||
tile = tileParent(tile, 0, r.tree.N)
|
||||
if j, ok := tileOrder[tile]; ok {
|
||||
stxTileOrder[i] = j
|
||||
continue
|
||||
}
|
||||
stxTileOrder[i] = len(tiles)
|
||||
tileOrder[tile] = len(tiles)
|
||||
tiles = append(tiles, tile)
|
||||
}
|
||||
|
||||
// Plan to fetch tiles containing the indexes,
|
||||
// along with any parent tiles needed
|
||||
// for authentication. For most calls,
|
||||
// the parents are being fetched anyway.
|
||||
indexTileOrder := make([]int, len(indexes))
|
||||
for i, x := range indexes {
|
||||
if x >= StoredHashIndex(0, r.tree.N) {
|
||||
return nil, fmt.Errorf("indexes not in tree")
|
||||
}
|
||||
|
||||
tile, _, _ := tileForIndex(h, x)
|
||||
|
||||
// Walk up parent tiles until we find one we've requested.
|
||||
// That one will be authenticated.
|
||||
k := 0
|
||||
for ; ; k++ {
|
||||
p := tileParent(tile, k, r.tree.N)
|
||||
if j, ok := tileOrder[p]; ok {
|
||||
if k == 0 {
|
||||
indexTileOrder[i] = j
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Walk back down recording child tiles after parents.
|
||||
// This loop ends by revisiting the tile for this index
|
||||
// (tileParent(tile, 0, r.tree.N)) unless k == 0, in which
|
||||
// case the previous loop did it.
|
||||
for k--; k >= 0; k-- {
|
||||
p := tileParent(tile, k, r.tree.N)
|
||||
if p.W != 1<<uint(p.H) {
|
||||
// Only full tiles have parents.
|
||||
// This tile has a parent, so it must be full.
|
||||
return nil, fmt.Errorf("bad math in tileHashReader: %d %d %v", r.tree.N, x, p)
|
||||
}
|
||||
tileOrder[p] = len(tiles)
|
||||
if k == 0 {
|
||||
indexTileOrder[i] = len(tiles)
|
||||
}
|
||||
tiles = append(tiles, p)
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch all the tile data.
|
||||
data, err := r.tr.ReadTiles(tiles)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(data) != len(tiles) {
|
||||
return nil, fmt.Errorf("TileReader returned bad result slice (len=%d, want %d)", len(data), len(tiles))
|
||||
}
|
||||
for i, tile := range tiles {
|
||||
if len(data[i]) != tile.W*HashSize {
|
||||
return nil, fmt.Errorf("TileReader returned bad result slice (%v len=%d, want %d)", tile.Path(), len(data[i]), tile.W*HashSize)
|
||||
}
|
||||
}
|
||||
|
||||
// Authenticate the initial tiles against the tree hash.
|
||||
// They are arranged so that parents are authenticated before children.
|
||||
// First the tiles needed for the tree hash.
|
||||
th, err := HashFromTile(tiles[stxTileOrder[len(stx)-1]], data[stxTileOrder[len(stx)-1]], stx[len(stx)-1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i := len(stx) - 2; i >= 0; i-- {
|
||||
h, err := HashFromTile(tiles[stxTileOrder[i]], data[stxTileOrder[i]], stx[i])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
th = NodeHash(h, th)
|
||||
}
|
||||
if th != r.tree.Hash {
|
||||
// The tiles do not support the tree hash.
|
||||
// We know at least one is wrong, but not which one.
|
||||
return nil, fmt.Errorf("downloaded inconsistent tile")
|
||||
}
|
||||
|
||||
// Authenticate full tiles against their parents.
|
||||
for i := len(stx); i < len(tiles); i++ {
|
||||
tile := tiles[i]
|
||||
p := tileParent(tile, 1, r.tree.N)
|
||||
j, ok := tileOrder[p]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("bad math in tileHashReader %d %v: lost parent of %v", r.tree.N, indexes, tile)
|
||||
}
|
||||
h, err := HashFromTile(p, data[j], StoredHashIndex(p.L*p.H, tile.N))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bad math in tileHashReader %d %v: lost hash of %v: %v", r.tree.N, indexes, tile, err)
|
||||
}
|
||||
if h != tileHash(data[i]) {
|
||||
return nil, fmt.Errorf("downloaded inconsistent tile")
|
||||
}
|
||||
}
|
||||
|
||||
// Now we have all the tiles needed for the requested hashes,
|
||||
// and we've authenticated the full tile set against the trusted tree hash.
|
||||
r.tr.SaveTiles(tiles, data)
|
||||
|
||||
// Pull out the requested hashes.
|
||||
hashes := make([]Hash, len(indexes))
|
||||
for i, x := range indexes {
|
||||
j := indexTileOrder[i]
|
||||
h, err := HashFromTile(tiles[j], data[j], x)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bad math in tileHashReader %d %v: lost hash %v: %v", r.tree.N, indexes, x, err)
|
||||
}
|
||||
hashes[i] = h
|
||||
}
|
||||
|
||||
return hashes, nil
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// Copyright 2019 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 tlog
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// FuzzParseTilePath tests that ParseTilePath never crashes
|
||||
func FuzzParseTilePath(f *testing.F) {
|
||||
f.Add("tile/4/0/001")
|
||||
f.Add("tile/4/0/001.p/5")
|
||||
f.Add("tile/3/5/x123/x456/078")
|
||||
f.Add("tile/3/5/x123/x456/078.p/2")
|
||||
f.Add("tile/1/0/x003/x057/500")
|
||||
f.Add("tile/3/5/123/456/078")
|
||||
f.Add("tile/3/-1/123/456/078")
|
||||
f.Add("tile/1/data/x003/x057/500")
|
||||
f.Fuzz(func(t *testing.T, path string) {
|
||||
ParseTilePath(path)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,597 @@
|
||||
// Copyright 2019 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 tlog implements a tamper-evident log
|
||||
// used in the Go module go.sum database server.
|
||||
//
|
||||
// This package follows the design of Certificate Transparency (RFC 6962)
|
||||
// and its proofs are compatible with that system.
|
||||
// See TestCertificateTransparency.
|
||||
package tlog
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/bits"
|
||||
)
|
||||
|
||||
// A Hash is a hash identifying a log record or tree root.
|
||||
type Hash [HashSize]byte
|
||||
|
||||
// HashSize is the size of a Hash in bytes.
|
||||
const HashSize = 32
|
||||
|
||||
// String returns a base64 representation of the hash for printing.
|
||||
func (h Hash) String() string {
|
||||
return base64.StdEncoding.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
// MarshalJSON marshals the hash as a JSON string containing the base64-encoded hash.
|
||||
func (h Hash) MarshalJSON() ([]byte, error) {
|
||||
return []byte(`"` + h.String() + `"`), nil
|
||||
}
|
||||
|
||||
// UnmarshalJSON unmarshals a hash from JSON string containing the a base64-encoded hash.
|
||||
func (h *Hash) UnmarshalJSON(data []byte) error {
|
||||
if len(data) != 1+44+1 || data[0] != '"' || data[len(data)-2] != '=' || data[len(data)-1] != '"' {
|
||||
return errors.New("cannot decode hash")
|
||||
}
|
||||
|
||||
// As of Go 1.12, base64.StdEncoding.Decode insists on
|
||||
// slicing into target[33:] even when it only writes 32 bytes.
|
||||
// Since we already checked that the hash ends in = above,
|
||||
// we can use base64.RawStdEncoding with the = removed;
|
||||
// RawStdEncoding does not exhibit the same bug.
|
||||
// We decode into a temporary to avoid writing anything to *h
|
||||
// unless the entire input is well-formed.
|
||||
var tmp Hash
|
||||
n, err := base64.RawStdEncoding.Decode(tmp[:], data[1:len(data)-2])
|
||||
if err != nil || n != HashSize {
|
||||
return errors.New("cannot decode hash")
|
||||
}
|
||||
*h = tmp
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseHash parses the base64-encoded string form of a hash.
|
||||
func ParseHash(s string) (Hash, error) {
|
||||
data, err := base64.StdEncoding.DecodeString(s)
|
||||
if err != nil || len(data) != HashSize {
|
||||
return Hash{}, fmt.Errorf("malformed hash")
|
||||
}
|
||||
var h Hash
|
||||
copy(h[:], data)
|
||||
return h, nil
|
||||
}
|
||||
|
||||
// maxpow2 returns k, the maximum power of 2 smaller than n,
|
||||
// as well as l = log₂ k (so k = 1<<l).
|
||||
func maxpow2(n int64) (k int64, l int) {
|
||||
l = 0
|
||||
for 1<<uint(l+1) < n {
|
||||
l++
|
||||
}
|
||||
return 1 << uint(l), l
|
||||
}
|
||||
|
||||
var zeroPrefix = []byte{0x00}
|
||||
|
||||
// RecordHash returns the content hash for the given record data.
|
||||
func RecordHash(data []byte) Hash {
|
||||
// SHA256(0x00 || data)
|
||||
// https://tools.ietf.org/html/rfc6962#section-2.1
|
||||
h := sha256.New()
|
||||
h.Write(zeroPrefix)
|
||||
h.Write(data)
|
||||
var h1 Hash
|
||||
h.Sum(h1[:0])
|
||||
return h1
|
||||
}
|
||||
|
||||
// NodeHash returns the hash for an interior tree node with the given left and right hashes.
|
||||
func NodeHash(left, right Hash) Hash {
|
||||
// SHA256(0x01 || left || right)
|
||||
// https://tools.ietf.org/html/rfc6962#section-2.1
|
||||
// We use a stack buffer to assemble the hash input
|
||||
// to avoid allocating a hash struct with sha256.New.
|
||||
var buf [1 + HashSize + HashSize]byte
|
||||
buf[0] = 0x01
|
||||
copy(buf[1:], left[:])
|
||||
copy(buf[1+HashSize:], right[:])
|
||||
return sha256.Sum256(buf[:])
|
||||
}
|
||||
|
||||
// For information about the stored hash index ordering,
|
||||
// see section 3.3 of Crosby and Wallach's paper
|
||||
// "Efficient Data Structures for Tamper-Evident Logging".
|
||||
// https://www.usenix.org/legacy/event/sec09/tech/full_papers/crosby.pdf
|
||||
|
||||
// StoredHashIndex maps the tree coordinates (level, n)
|
||||
// to a dense linear ordering that can be used for hash storage.
|
||||
// Hash storage implementations that store hashes in sequential
|
||||
// storage can use this function to compute where to read or write
|
||||
// a given hash.
|
||||
func StoredHashIndex(level int, n int64) int64 {
|
||||
// Level L's n'th hash is written right after level L+1's 2n+1'th hash.
|
||||
// Work our way down to the level 0 ordering.
|
||||
// We'll add back the original level count at the end.
|
||||
for l := level; l > 0; l-- {
|
||||
n = 2*n + 1
|
||||
}
|
||||
|
||||
// Level 0's n'th hash is written at n+n/2+n/4+... (eventually n/2ⁱ hits zero).
|
||||
i := int64(0)
|
||||
for ; n > 0; n >>= 1 {
|
||||
i += n
|
||||
}
|
||||
|
||||
return i + int64(level)
|
||||
}
|
||||
|
||||
// SplitStoredHashIndex is the inverse of [StoredHashIndex].
|
||||
// That is, SplitStoredHashIndex(StoredHashIndex(level, n)) == level, n.
|
||||
func SplitStoredHashIndex(index int64) (level int, n int64) {
|
||||
// Determine level 0 record before index.
|
||||
// StoredHashIndex(0, n) < 2*n,
|
||||
// so the n we want is in [index/2, index/2+log₂(index)].
|
||||
n = index / 2
|
||||
indexN := StoredHashIndex(0, n)
|
||||
if indexN > index {
|
||||
panic("bad math")
|
||||
}
|
||||
for {
|
||||
// Each new record n adds 1 + trailingZeros(n) hashes.
|
||||
x := indexN + 1 + int64(bits.TrailingZeros64(uint64(n+1)))
|
||||
if x > index {
|
||||
break
|
||||
}
|
||||
n++
|
||||
indexN = x
|
||||
}
|
||||
// The hash we want was committed with record n,
|
||||
// meaning it is one of (0, n), (1, n/2), (2, n/4), ...
|
||||
level = int(index - indexN)
|
||||
return level, n >> uint(level)
|
||||
}
|
||||
|
||||
// StoredHashCount returns the number of stored hashes
|
||||
// that are expected for a tree with n records.
|
||||
func StoredHashCount(n int64) int64 {
|
||||
if n == 0 {
|
||||
return 0
|
||||
}
|
||||
// The tree will have the hashes up to the last leaf hash.
|
||||
numHash := StoredHashIndex(0, n-1) + 1
|
||||
// And it will have any hashes for subtrees completed by that leaf.
|
||||
for i := uint64(n - 1); i&1 != 0; i >>= 1 {
|
||||
numHash++
|
||||
}
|
||||
return numHash
|
||||
}
|
||||
|
||||
// StoredHashes returns the hashes that must be stored when writing
|
||||
// record n with the given data. The hashes should be stored starting
|
||||
// at StoredHashIndex(0, n). The result will have at most 1 + log₂ n hashes,
|
||||
// but it will average just under two per call for a sequence of calls for n=1..k.
|
||||
//
|
||||
// StoredHashes may read up to log n earlier hashes from r
|
||||
// in order to compute hashes for completed subtrees.
|
||||
func StoredHashes(n int64, data []byte, r HashReader) ([]Hash, error) {
|
||||
return StoredHashesForRecordHash(n, RecordHash(data), r)
|
||||
}
|
||||
|
||||
// StoredHashesForRecordHash is like [StoredHashes] but takes
|
||||
// as its second argument RecordHash(data) instead of data itself.
|
||||
func StoredHashesForRecordHash(n int64, h Hash, r HashReader) ([]Hash, error) {
|
||||
// Start with the record hash.
|
||||
hashes := []Hash{h}
|
||||
|
||||
// Build list of indexes needed for hashes for completed subtrees.
|
||||
// Each trailing 1 bit in the binary representation of n completes a subtree
|
||||
// and consumes a hash from an adjacent subtree.
|
||||
m := int(bits.TrailingZeros64(uint64(n + 1)))
|
||||
indexes := make([]int64, m)
|
||||
for i := 0; i < m; i++ {
|
||||
// We arrange indexes in sorted order.
|
||||
// Note that n>>i is always odd.
|
||||
indexes[m-1-i] = StoredHashIndex(i, n>>uint(i)-1)
|
||||
}
|
||||
|
||||
// Fetch hashes.
|
||||
old, err := r.ReadHashes(indexes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(old) != len(indexes) {
|
||||
return nil, fmt.Errorf("tlog: ReadHashes(%d indexes) = %d hashes", len(indexes), len(old))
|
||||
}
|
||||
|
||||
// Build new hashes.
|
||||
for i := 0; i < m; i++ {
|
||||
h = NodeHash(old[m-1-i], h)
|
||||
hashes = append(hashes, h)
|
||||
}
|
||||
return hashes, nil
|
||||
}
|
||||
|
||||
// A HashReader can read hashes for nodes in the log's tree structure.
|
||||
type HashReader interface {
|
||||
// ReadHashes returns the hashes with the given stored hash indexes
|
||||
// (see StoredHashIndex and SplitStoredHashIndex).
|
||||
// ReadHashes must return a slice of hashes the same length as indexes,
|
||||
// or else it must return a non-nil error.
|
||||
// ReadHashes may run faster if indexes is sorted in increasing order.
|
||||
ReadHashes(indexes []int64) ([]Hash, error)
|
||||
}
|
||||
|
||||
// A HashReaderFunc is a function implementing [HashReader].
|
||||
type HashReaderFunc func([]int64) ([]Hash, error)
|
||||
|
||||
func (f HashReaderFunc) ReadHashes(indexes []int64) ([]Hash, error) {
|
||||
return f(indexes)
|
||||
}
|
||||
|
||||
// TreeHash computes the hash for the root of the tree with n records,
|
||||
// using the HashReader to obtain previously stored hashes
|
||||
// (those returned by StoredHashes during the writes of those n records).
|
||||
// TreeHash makes a single call to ReadHash requesting at most 1 + log₂ n hashes.
|
||||
// The tree of size zero is defined to have an all-zero Hash.
|
||||
func TreeHash(n int64, r HashReader) (Hash, error) {
|
||||
if n == 0 {
|
||||
return Hash{}, nil
|
||||
}
|
||||
indexes := subTreeIndex(0, n, nil)
|
||||
hashes, err := r.ReadHashes(indexes)
|
||||
if err != nil {
|
||||
return Hash{}, err
|
||||
}
|
||||
if len(hashes) != len(indexes) {
|
||||
return Hash{}, fmt.Errorf("tlog: ReadHashes(%d indexes) = %d hashes", len(indexes), len(hashes))
|
||||
}
|
||||
hash, hashes := subTreeHash(0, n, hashes)
|
||||
if len(hashes) != 0 {
|
||||
panic("tlog: bad index math in TreeHash")
|
||||
}
|
||||
return hash, nil
|
||||
}
|
||||
|
||||
// subTreeIndex returns the storage indexes needed to compute
|
||||
// the hash for the subtree containing records [lo, hi),
|
||||
// appending them to need and returning the result.
|
||||
// See https://tools.ietf.org/html/rfc6962#section-2.1
|
||||
func subTreeIndex(lo, hi int64, need []int64) []int64 {
|
||||
// See subTreeHash below for commentary.
|
||||
for lo < hi {
|
||||
k, level := maxpow2(hi - lo + 1)
|
||||
if lo&(k-1) != 0 {
|
||||
panic("tlog: bad math in subTreeIndex")
|
||||
}
|
||||
need = append(need, StoredHashIndex(level, lo>>uint(level)))
|
||||
lo += k
|
||||
}
|
||||
return need
|
||||
}
|
||||
|
||||
// subTreeHash computes the hash for the subtree containing records [lo, hi),
|
||||
// assuming that hashes are the hashes corresponding to the indexes
|
||||
// returned by subTreeIndex(lo, hi).
|
||||
// It returns any leftover hashes.
|
||||
func subTreeHash(lo, hi int64, hashes []Hash) (Hash, []Hash) {
|
||||
// Repeatedly partition the tree into a left side with 2^level nodes,
|
||||
// for as large a level as possible, and a right side with the fringe.
|
||||
// The left hash is stored directly and can be read from storage.
|
||||
// The right side needs further computation.
|
||||
numTree := 0
|
||||
for lo < hi {
|
||||
k, _ := maxpow2(hi - lo + 1)
|
||||
if lo&(k-1) != 0 || lo >= hi {
|
||||
panic("tlog: bad math in subTreeHash")
|
||||
}
|
||||
numTree++
|
||||
lo += k
|
||||
}
|
||||
|
||||
if len(hashes) < numTree {
|
||||
panic("tlog: bad index math in subTreeHash")
|
||||
}
|
||||
|
||||
// Reconstruct hash.
|
||||
h := hashes[numTree-1]
|
||||
for i := numTree - 2; i >= 0; i-- {
|
||||
h = NodeHash(hashes[i], h)
|
||||
}
|
||||
return h, hashes[numTree:]
|
||||
}
|
||||
|
||||
// A RecordProof is a verifiable proof that a particular log root contains a particular record.
|
||||
// RFC 6962 calls this a “Merkle audit path.”
|
||||
type RecordProof []Hash
|
||||
|
||||
// ProveRecord returns the proof that the tree of size t contains the record with index n.
|
||||
func ProveRecord(t, n int64, r HashReader) (RecordProof, error) {
|
||||
if t < 0 || n < 0 || n >= t {
|
||||
return nil, fmt.Errorf("tlog: invalid inputs in ProveRecord")
|
||||
}
|
||||
indexes := leafProofIndex(0, t, n, nil)
|
||||
if len(indexes) == 0 {
|
||||
return RecordProof{}, nil
|
||||
}
|
||||
hashes, err := r.ReadHashes(indexes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(hashes) != len(indexes) {
|
||||
return nil, fmt.Errorf("tlog: ReadHashes(%d indexes) = %d hashes", len(indexes), len(hashes))
|
||||
}
|
||||
|
||||
p, hashes := leafProof(0, t, n, hashes)
|
||||
if len(hashes) != 0 {
|
||||
panic("tlog: bad index math in ProveRecord")
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// leafProofIndex builds the list of indexes needed to construct the proof
|
||||
// that leaf n is contained in the subtree with leaves [lo, hi).
|
||||
// It appends those indexes to need and returns the result.
|
||||
// See https://tools.ietf.org/html/rfc6962#section-2.1.1
|
||||
func leafProofIndex(lo, hi, n int64, need []int64) []int64 {
|
||||
// See leafProof below for commentary.
|
||||
if !(lo <= n && n < hi) {
|
||||
panic("tlog: bad math in leafProofIndex")
|
||||
}
|
||||
if lo+1 == hi {
|
||||
return need
|
||||
}
|
||||
if k, _ := maxpow2(hi - lo); n < lo+k {
|
||||
need = leafProofIndex(lo, lo+k, n, need)
|
||||
need = subTreeIndex(lo+k, hi, need)
|
||||
} else {
|
||||
need = subTreeIndex(lo, lo+k, need)
|
||||
need = leafProofIndex(lo+k, hi, n, need)
|
||||
}
|
||||
return need
|
||||
}
|
||||
|
||||
// leafProof constructs the proof that leaf n is contained in the subtree with leaves [lo, hi).
|
||||
// It returns any leftover hashes as well.
|
||||
// See https://tools.ietf.org/html/rfc6962#section-2.1.1
|
||||
func leafProof(lo, hi, n int64, hashes []Hash) (RecordProof, []Hash) {
|
||||
// We must have lo <= n < hi or else the code here has a bug.
|
||||
if !(lo <= n && n < hi) {
|
||||
panic("tlog: bad math in leafProof")
|
||||
}
|
||||
|
||||
if lo+1 == hi { // n == lo
|
||||
// Reached the leaf node.
|
||||
// The verifier knows what the leaf hash is, so we don't need to send it.
|
||||
return RecordProof{}, hashes
|
||||
}
|
||||
|
||||
// Walk down the tree toward n.
|
||||
// Record the hash of the path not taken (needed for verifying the proof).
|
||||
var p RecordProof
|
||||
var th Hash
|
||||
if k, _ := maxpow2(hi - lo); n < lo+k {
|
||||
// n is on left side
|
||||
p, hashes = leafProof(lo, lo+k, n, hashes)
|
||||
th, hashes = subTreeHash(lo+k, hi, hashes)
|
||||
} else {
|
||||
// n is on right side
|
||||
th, hashes = subTreeHash(lo, lo+k, hashes)
|
||||
p, hashes = leafProof(lo+k, hi, n, hashes)
|
||||
}
|
||||
return append(p, th), hashes
|
||||
}
|
||||
|
||||
var errProofFailed = errors.New("invalid transparency proof")
|
||||
|
||||
// CheckRecord verifies that p is a valid proof that the tree of size t
|
||||
// with hash th has an n'th record with hash h.
|
||||
func CheckRecord(p RecordProof, t int64, th Hash, n int64, h Hash) error {
|
||||
if t < 0 || n < 0 || n >= t {
|
||||
return fmt.Errorf("tlog: invalid inputs in CheckRecord")
|
||||
}
|
||||
th2, err := runRecordProof(p, 0, t, n, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if th2 == th {
|
||||
return nil
|
||||
}
|
||||
return errProofFailed
|
||||
}
|
||||
|
||||
// runRecordProof runs the proof p that leaf n is contained in the subtree with leaves [lo, hi).
|
||||
// Running the proof means constructing and returning the implied hash of that
|
||||
// subtree.
|
||||
func runRecordProof(p RecordProof, lo, hi, n int64, leafHash Hash) (Hash, error) {
|
||||
// We must have lo <= n < hi or else the code here has a bug.
|
||||
if !(lo <= n && n < hi) {
|
||||
panic("tlog: bad math in runRecordProof")
|
||||
}
|
||||
|
||||
if lo+1 == hi { // m == lo
|
||||
// Reached the leaf node.
|
||||
// The proof must not have any unnecessary hashes.
|
||||
if len(p) != 0 {
|
||||
return Hash{}, errProofFailed
|
||||
}
|
||||
return leafHash, nil
|
||||
}
|
||||
|
||||
if len(p) == 0 {
|
||||
return Hash{}, errProofFailed
|
||||
}
|
||||
|
||||
k, _ := maxpow2(hi - lo)
|
||||
if n < lo+k {
|
||||
th, err := runRecordProof(p[:len(p)-1], lo, lo+k, n, leafHash)
|
||||
if err != nil {
|
||||
return Hash{}, err
|
||||
}
|
||||
return NodeHash(th, p[len(p)-1]), nil
|
||||
} else {
|
||||
th, err := runRecordProof(p[:len(p)-1], lo+k, hi, n, leafHash)
|
||||
if err != nil {
|
||||
return Hash{}, err
|
||||
}
|
||||
return NodeHash(p[len(p)-1], th), nil
|
||||
}
|
||||
}
|
||||
|
||||
// A TreeProof is a verifiable proof that a particular log tree contains
|
||||
// as a prefix all records present in an earlier tree.
|
||||
// RFC 6962 calls this a “Merkle consistency proof.”
|
||||
type TreeProof []Hash
|
||||
|
||||
// ProveTree returns the proof that the tree of size t contains
|
||||
// as a prefix all the records from the tree of smaller size n.
|
||||
func ProveTree(t, n int64, h HashReader) (TreeProof, error) {
|
||||
if t < 1 || n < 1 || n > t {
|
||||
return nil, fmt.Errorf("tlog: invalid inputs in ProveTree")
|
||||
}
|
||||
indexes := treeProofIndex(0, t, n, nil)
|
||||
if len(indexes) == 0 {
|
||||
return TreeProof{}, nil
|
||||
}
|
||||
hashes, err := h.ReadHashes(indexes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(hashes) != len(indexes) {
|
||||
return nil, fmt.Errorf("tlog: ReadHashes(%d indexes) = %d hashes", len(indexes), len(hashes))
|
||||
}
|
||||
|
||||
p, hashes := treeProof(0, t, n, hashes)
|
||||
if len(hashes) != 0 {
|
||||
panic("tlog: bad index math in ProveTree")
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// treeProofIndex builds the list of indexes needed to construct
|
||||
// the sub-proof related to the subtree containing records [lo, hi).
|
||||
// See https://tools.ietf.org/html/rfc6962#section-2.1.2.
|
||||
func treeProofIndex(lo, hi, n int64, need []int64) []int64 {
|
||||
// See treeProof below for commentary.
|
||||
if !(lo < n && n <= hi) {
|
||||
panic("tlog: bad math in treeProofIndex")
|
||||
}
|
||||
|
||||
if n == hi {
|
||||
if lo == 0 {
|
||||
return need
|
||||
}
|
||||
return subTreeIndex(lo, hi, need)
|
||||
}
|
||||
|
||||
if k, _ := maxpow2(hi - lo); n <= lo+k {
|
||||
need = treeProofIndex(lo, lo+k, n, need)
|
||||
need = subTreeIndex(lo+k, hi, need)
|
||||
} else {
|
||||
need = subTreeIndex(lo, lo+k, need)
|
||||
need = treeProofIndex(lo+k, hi, n, need)
|
||||
}
|
||||
return need
|
||||
}
|
||||
|
||||
// treeProof constructs the sub-proof related to the subtree containing records [lo, hi).
|
||||
// It returns any leftover hashes as well.
|
||||
// See https://tools.ietf.org/html/rfc6962#section-2.1.2.
|
||||
func treeProof(lo, hi, n int64, hashes []Hash) (TreeProof, []Hash) {
|
||||
// We must have lo < n <= hi or else the code here has a bug.
|
||||
if !(lo < n && n <= hi) {
|
||||
panic("tlog: bad math in treeProof")
|
||||
}
|
||||
|
||||
// Reached common ground.
|
||||
if n == hi {
|
||||
if lo == 0 {
|
||||
// This subtree corresponds exactly to the old tree.
|
||||
// The verifier knows that hash, so we don't need to send it.
|
||||
return TreeProof{}, hashes
|
||||
}
|
||||
th, hashes := subTreeHash(lo, hi, hashes)
|
||||
return TreeProof{th}, hashes
|
||||
}
|
||||
|
||||
// Interior node for the proof.
|
||||
// Decide whether to walk down the left or right side.
|
||||
var p TreeProof
|
||||
var th Hash
|
||||
if k, _ := maxpow2(hi - lo); n <= lo+k {
|
||||
// m is on left side
|
||||
p, hashes = treeProof(lo, lo+k, n, hashes)
|
||||
th, hashes = subTreeHash(lo+k, hi, hashes)
|
||||
} else {
|
||||
// m is on right side
|
||||
th, hashes = subTreeHash(lo, lo+k, hashes)
|
||||
p, hashes = treeProof(lo+k, hi, n, hashes)
|
||||
}
|
||||
return append(p, th), hashes
|
||||
}
|
||||
|
||||
// CheckTree verifies that p is a valid proof that the tree of size t with hash th
|
||||
// contains as a prefix the tree of size n with hash h.
|
||||
func CheckTree(p TreeProof, t int64, th Hash, n int64, h Hash) error {
|
||||
if t < 1 || n < 1 || n > t {
|
||||
return fmt.Errorf("tlog: invalid inputs in CheckTree")
|
||||
}
|
||||
h2, th2, err := runTreeProof(p, 0, t, n, h)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if th2 == th && h2 == h {
|
||||
return nil
|
||||
}
|
||||
return errProofFailed
|
||||
}
|
||||
|
||||
// runTreeProof runs the sub-proof p related to the subtree containing records [lo, hi),
|
||||
// where old is the hash of the old tree with n records.
|
||||
// Running the proof means constructing and returning the implied hashes of that
|
||||
// subtree in both the old and new tree.
|
||||
func runTreeProof(p TreeProof, lo, hi, n int64, old Hash) (Hash, Hash, error) {
|
||||
// We must have lo < n <= hi or else the code here has a bug.
|
||||
if !(lo < n && n <= hi) {
|
||||
panic("tlog: bad math in runTreeProof")
|
||||
}
|
||||
|
||||
// Reached common ground.
|
||||
if n == hi {
|
||||
if lo == 0 {
|
||||
if len(p) != 0 {
|
||||
return Hash{}, Hash{}, errProofFailed
|
||||
}
|
||||
return old, old, nil
|
||||
}
|
||||
if len(p) != 1 {
|
||||
return Hash{}, Hash{}, errProofFailed
|
||||
}
|
||||
return p[0], p[0], nil
|
||||
}
|
||||
|
||||
if len(p) == 0 {
|
||||
return Hash{}, Hash{}, errProofFailed
|
||||
}
|
||||
|
||||
// Interior node for the proof.
|
||||
k, _ := maxpow2(hi - lo)
|
||||
if n <= lo+k {
|
||||
oh, th, err := runTreeProof(p[:len(p)-1], lo, lo+k, n, old)
|
||||
if err != nil {
|
||||
return Hash{}, Hash{}, err
|
||||
}
|
||||
return oh, NodeHash(th, p[len(p)-1]), nil
|
||||
} else {
|
||||
oh, th, err := runTreeProof(p[:len(p)-1], lo+k, hi, n, old)
|
||||
if err != nil {
|
||||
return Hash{}, Hash{}, err
|
||||
}
|
||||
return NodeHash(p[len(p)-1], oh), NodeHash(p[len(p)-1], th), nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
// Copyright 2019 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 tlog
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testHashStorage []Hash
|
||||
|
||||
func (t testHashStorage) ReadHash(level int, n int64) (Hash, error) {
|
||||
return t[StoredHashIndex(level, n)], nil
|
||||
}
|
||||
|
||||
func (t testHashStorage) ReadHashes(index []int64) ([]Hash, error) {
|
||||
// It's not required by HashReader that indexes be in increasing order,
|
||||
// but check that the functions we are testing only ever ask for
|
||||
// indexes in increasing order.
|
||||
for i := 1; i < len(index); i++ {
|
||||
if index[i-1] >= index[i] {
|
||||
panic("indexes out of order")
|
||||
}
|
||||
}
|
||||
|
||||
out := make([]Hash, len(index))
|
||||
for i, x := range index {
|
||||
out[i] = t[x]
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type testTilesStorage struct {
|
||||
unsaved int
|
||||
m map[Tile][]byte
|
||||
}
|
||||
|
||||
func (t testTilesStorage) Height() int {
|
||||
return 2
|
||||
}
|
||||
|
||||
func (t *testTilesStorage) SaveTiles(tiles []Tile, data [][]byte) {
|
||||
t.unsaved -= len(tiles)
|
||||
}
|
||||
|
||||
func (t *testTilesStorage) ReadTiles(tiles []Tile) ([][]byte, error) {
|
||||
out := make([][]byte, len(tiles))
|
||||
for i, tile := range tiles {
|
||||
out[i] = t.m[tile]
|
||||
}
|
||||
t.unsaved += len(tiles)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func TestTree(t *testing.T) {
|
||||
var trees []Hash
|
||||
var leafhashes []Hash
|
||||
var storage testHashStorage
|
||||
tiles := make(map[Tile][]byte)
|
||||
const testH = 2
|
||||
for i := int64(0); i < 100; i++ {
|
||||
data := []byte(fmt.Sprintf("leaf %d", i))
|
||||
hashes, err := StoredHashes(i, data, storage)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
leafhashes = append(leafhashes, RecordHash(data))
|
||||
oldStorage := len(storage)
|
||||
storage = append(storage, hashes...)
|
||||
if count := StoredHashCount(i + 1); count != int64(len(storage)) {
|
||||
t.Errorf("StoredHashCount(%d) = %d, have %d StoredHashes", i+1, count, len(storage))
|
||||
}
|
||||
th, err := TreeHash(i+1, storage)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for _, tile := range NewTiles(testH, i, i+1) {
|
||||
data, err := ReadTileData(tile, storage)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
old := Tile{H: tile.H, L: tile.L, N: tile.N, W: tile.W - 1}
|
||||
oldData := tiles[old]
|
||||
if len(oldData) != len(data)-HashSize || !bytes.Equal(oldData, data[:len(oldData)]) {
|
||||
t.Fatalf("tile %v not extending earlier tile %v", tile.Path(), old.Path())
|
||||
}
|
||||
tiles[tile] = data
|
||||
}
|
||||
for _, tile := range NewTiles(testH, 0, i+1) {
|
||||
data, err := ReadTileData(tile, storage)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(tiles[tile], data) {
|
||||
t.Fatalf("mismatch at %+v", tile)
|
||||
}
|
||||
}
|
||||
for _, tile := range NewTiles(testH, i/2, i+1) {
|
||||
data, err := ReadTileData(tile, storage)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(tiles[tile], data) {
|
||||
t.Fatalf("mismatch at %+v", tile)
|
||||
}
|
||||
}
|
||||
|
||||
// Check that all the new hashes are readable from their tiles.
|
||||
for j := oldStorage; j < len(storage); j++ {
|
||||
tile := TileForIndex(testH, int64(j))
|
||||
data, ok := tiles[tile]
|
||||
if !ok {
|
||||
t.Log(NewTiles(testH, 0, i+1))
|
||||
t.Fatalf("TileForIndex(%d, %d) = %v, not yet stored (i=%d, stored %d)", testH, j, tile.Path(), i, len(storage))
|
||||
continue
|
||||
}
|
||||
h, err := HashFromTile(tile, data, int64(j))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h != storage[j] {
|
||||
t.Errorf("HashFromTile(%v, %d) = %v, want %v", tile.Path(), int64(j), h, storage[j])
|
||||
}
|
||||
}
|
||||
|
||||
trees = append(trees, th)
|
||||
|
||||
// Check that leaf proofs work, for all trees and leaves so far.
|
||||
for j := int64(0); j <= i; j++ {
|
||||
p, err := ProveRecord(i+1, j, storage)
|
||||
if err != nil {
|
||||
t.Fatalf("ProveRecord(%d, %d): %v", i+1, j, err)
|
||||
}
|
||||
if err := CheckRecord(p, i+1, th, j, leafhashes[j]); err != nil {
|
||||
t.Fatalf("CheckRecord(%d, %d): %v", i+1, j, err)
|
||||
}
|
||||
for k := range p {
|
||||
p[k][0] ^= 1
|
||||
if err := CheckRecord(p, i+1, th, j, leafhashes[j]); err == nil {
|
||||
t.Fatalf("CheckRecord(%d, %d) succeeded with corrupt proof hash #%d!", i+1, j, k)
|
||||
}
|
||||
p[k][0] ^= 1
|
||||
}
|
||||
}
|
||||
|
||||
// Check that leaf proofs work using TileReader.
|
||||
// To prove a leaf that way, all you have to do is read and verify its hash.
|
||||
storage := &testTilesStorage{m: tiles}
|
||||
thr := TileHashReader(Tree{i + 1, th}, storage)
|
||||
for j := int64(0); j <= i; j++ {
|
||||
h, err := thr.ReadHashes([]int64{StoredHashIndex(0, j)})
|
||||
if err != nil {
|
||||
t.Fatalf("TileHashReader(%d).ReadHashes(%d): %v", i+1, j, err)
|
||||
}
|
||||
if h[0] != leafhashes[j] {
|
||||
t.Fatalf("TileHashReader(%d).ReadHashes(%d) returned wrong hash", i+1, j)
|
||||
}
|
||||
|
||||
// Even though reading the hash suffices,
|
||||
// check we can generate the proof too.
|
||||
p, err := ProveRecord(i+1, j, thr)
|
||||
if err != nil {
|
||||
t.Fatalf("ProveRecord(%d, %d, TileHashReader(%d)): %v", i+1, j, i+1, err)
|
||||
}
|
||||
if err := CheckRecord(p, i+1, th, j, leafhashes[j]); err != nil {
|
||||
t.Fatalf("CheckRecord(%d, %d, TileHashReader(%d)): %v", i+1, j, i+1, err)
|
||||
}
|
||||
}
|
||||
if storage.unsaved != 0 {
|
||||
t.Fatalf("TileHashReader(%d) did not save %d tiles", i+1, storage.unsaved)
|
||||
}
|
||||
|
||||
// Check that ReadHashes will give an error if the index is not in the tree.
|
||||
if _, err := thr.ReadHashes([]int64{(i + 1) * 2}); err == nil {
|
||||
t.Fatalf("TileHashReader(%d).ReadHashes(%d) for index not in tree <nil>, want err", i, i+1)
|
||||
}
|
||||
if storage.unsaved != 0 {
|
||||
t.Fatalf("TileHashReader(%d) did not save %d tiles", i+1, storage.unsaved)
|
||||
}
|
||||
|
||||
// Check that tree proofs work, for all trees so far, using TileReader.
|
||||
// To prove a tree that way, all you have to do is compute and verify its hash.
|
||||
for j := int64(0); j <= i; j++ {
|
||||
h, err := TreeHash(j+1, thr)
|
||||
if err != nil {
|
||||
t.Fatalf("TreeHash(%d, TileHashReader(%d)): %v", j, i+1, err)
|
||||
}
|
||||
if h != trees[j] {
|
||||
t.Fatalf("TreeHash(%d, TileHashReader(%d)) = %x, want %x (%v)", j, i+1, h[:], trees[j][:], trees[j])
|
||||
}
|
||||
|
||||
// Even though computing the subtree hash suffices,
|
||||
// check that we can generate the proof too.
|
||||
p, err := ProveTree(i+1, j+1, thr)
|
||||
if err != nil {
|
||||
t.Fatalf("ProveTree(%d, %d): %v", i+1, j+1, err)
|
||||
}
|
||||
if err := CheckTree(p, i+1, th, j+1, trees[j]); err != nil {
|
||||
t.Fatalf("CheckTree(%d, %d): %v [%v]", i+1, j+1, err, p)
|
||||
}
|
||||
for k := range p {
|
||||
p[k][0] ^= 1
|
||||
if err := CheckTree(p, i+1, th, j+1, trees[j]); err == nil {
|
||||
t.Fatalf("CheckTree(%d, %d) succeeded with corrupt proof hash #%d!", i+1, j+1, k)
|
||||
}
|
||||
p[k][0] ^= 1
|
||||
}
|
||||
}
|
||||
if storage.unsaved != 0 {
|
||||
t.Fatalf("TileHashReader(%d) did not save %d tiles", i+1, storage.unsaved)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitStoredHashIndex(t *testing.T) {
|
||||
for l := 0; l < 10; l++ {
|
||||
for n := int64(0); n < 100; n++ {
|
||||
x := StoredHashIndex(l, n)
|
||||
l1, n1 := SplitStoredHashIndex(x)
|
||||
if l1 != l || n1 != n {
|
||||
t.Fatalf("StoredHashIndex(%d, %d) = %d, but SplitStoredHashIndex(%d) = %d, %d", l, n, x, x, l1, n1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(rsc): Test invalid paths too, like "tile/3/5/123/456/078".
|
||||
var tilePaths = []struct {
|
||||
path string
|
||||
tile Tile
|
||||
}{
|
||||
{"tile/4/0/001", Tile{4, 0, 1, 16}},
|
||||
{"tile/4/0/001.p/5", Tile{4, 0, 1, 5}},
|
||||
{"tile/3/5/x123/x456/078", Tile{3, 5, 123456078, 8}},
|
||||
{"tile/3/5/x123/x456/078.p/2", Tile{3, 5, 123456078, 2}},
|
||||
{"tile/1/0/x003/x057/500", Tile{1, 0, 3057500, 2}},
|
||||
{"tile/3/5/123/456/078", Tile{}},
|
||||
{"tile/3/-1/123/456/078", Tile{}},
|
||||
{"tile/1/data/x003/x057/500", Tile{1, -1, 3057500, 2}},
|
||||
}
|
||||
|
||||
func TestTilePath(t *testing.T) {
|
||||
for _, tt := range tilePaths {
|
||||
if tt.tile.H > 0 {
|
||||
p := tt.tile.Path()
|
||||
if p != tt.path {
|
||||
t.Errorf("%+v.Path() = %q, want %q", tt.tile, p, tt.path)
|
||||
}
|
||||
}
|
||||
tile, err := ParseTilePath(tt.path)
|
||||
if err != nil {
|
||||
if tt.tile.H == 0 {
|
||||
// Expected error.
|
||||
continue
|
||||
}
|
||||
t.Errorf("ParseTilePath(%q): %v", tt.path, err)
|
||||
} else if tile != tt.tile {
|
||||
if tt.tile.H == 0 {
|
||||
t.Errorf("ParseTilePath(%q): expected error, got %+v", tt.path, tt.tile)
|
||||
continue
|
||||
}
|
||||
t.Errorf("ParseTilePath(%q) = %+v, want %+v", tt.path, tile, tt.tile)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
-- want --
|
||||
valid:
|
||||
|
||||
omitted:
|
||||
|
||||
invalid:
|
||||
@@ -0,0 +1,20 @@
|
||||
-- want --
|
||||
valid:
|
||||
$work/valid.go
|
||||
|
||||
omitted:
|
||||
$work/.hg_archival.txt: file is inserted by 'hg archive' and is always omitted
|
||||
$work/.git: directory is a version control repository
|
||||
$work/sub: directory is in another module
|
||||
$work/vendor/x/y: file is in vendor directory
|
||||
|
||||
invalid:
|
||||
$work/GO.MOD: go.mod files must have lowercase names
|
||||
$work/invalid.go': malformed file path "invalid.go'": invalid char '\''
|
||||
-- valid.go --
|
||||
-- GO.MOD --
|
||||
-- invalid.go' --
|
||||
-- vendor/x/y --
|
||||
-- sub/go.mod --
|
||||
-- .hg_archival.txt --
|
||||
-- .git/x --
|
||||
@@ -0,0 +1,6 @@
|
||||
-- want --
|
||||
valid:
|
||||
|
||||
omitted:
|
||||
|
||||
invalid:
|
||||
@@ -0,0 +1,25 @@
|
||||
-- want --
|
||||
valid:
|
||||
valid.go
|
||||
|
||||
omitted:
|
||||
vendor/x/y: file is in vendor directory
|
||||
sub/go.mod: file is in another module
|
||||
.hg_archival.txt: file is inserted by 'hg archive' and is always omitted
|
||||
|
||||
invalid:
|
||||
not/../clean: file path is not clean
|
||||
GO.MOD: go.mod files must have lowercase names
|
||||
invalid.go': malformed file path "invalid.go'": invalid char '\''
|
||||
valid.go: multiple entries for file "valid.go"
|
||||
-- valid.go --
|
||||
-- not/../clean --
|
||||
-- GO.MOD --
|
||||
-- invalid.go' --
|
||||
-- vendor/x/y --
|
||||
-- sub/go.mod --
|
||||
-- .hg_archival.txt --
|
||||
-- valid.go --
|
||||
duplicate
|
||||
-- valid.go --
|
||||
another duplicate
|
||||
@@ -0,0 +1,8 @@
|
||||
path=example.com/empty
|
||||
version=v1.0.0
|
||||
-- want --
|
||||
valid:
|
||||
|
||||
omitted:
|
||||
|
||||
invalid:
|
||||
@@ -0,0 +1,21 @@
|
||||
path=example.com/various
|
||||
version=v1.0.0
|
||||
-- want --
|
||||
valid:
|
||||
example.com/various@v1.0.0/valid.go
|
||||
|
||||
omitted:
|
||||
|
||||
invalid:
|
||||
noprefix: path does not have prefix "example.com/various@v1.0.0/"
|
||||
example.com/various@v1.0.0/not/../clean: file path is not clean
|
||||
example.com/various@v1.0.0/invalid.go': malformed file path "invalid.go'": invalid char '\''
|
||||
example.com/various@v1.0.0/GO.MOD: go.mod files must have lowercase names
|
||||
example.com/various@v1.0.0/valid.go: multiple entries for file "valid.go"
|
||||
-- noprefix --
|
||||
-- example.com/various@v1.0.0/valid.go --
|
||||
-- example.com/various@v1.0.0/not/../clean --
|
||||
-- example.com/various@v1.0.0/invalid.go' --
|
||||
-- example.com/various@v1.0.0/GO.MOD --
|
||||
-- example.com/various@v1.0.0/valid.go --
|
||||
duplicate
|
||||
@@ -0,0 +1,5 @@
|
||||
path=example.com/m
|
||||
version=v1.0.0
|
||||
wantErr=malformed file path "bad.go'": invalid char '\''
|
||||
-- bad.go' --
|
||||
package bad
|
||||
@@ -0,0 +1,5 @@
|
||||
path=example.com/m
|
||||
version=v1.0.0
|
||||
wantErr=GO.MOD: go.mod files must have lowercase names
|
||||
-- GO.MOD --
|
||||
module example.com/m
|
||||
@@ -0,0 +1,3 @@
|
||||
path=cache
|
||||
version=v1.0.0
|
||||
wantErr=missing dot in first path element
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
path=example.com/m
|
||||
version=v2.0.0
|
||||
wantErr=invalid version: should be v0 or v1, not v2
|
||||
@@ -0,0 +1,3 @@
|
||||
path=example.com/m
|
||||
version=v1.0.0+bad
|
||||
wantErr=version "v1.0.0+bad" is not canonical (should be "v1.0.0")
|
||||
@@ -0,0 +1,7 @@
|
||||
path=example.com/m
|
||||
version=v1.0.0
|
||||
wantErr=multiple entries for file "dup.go"
|
||||
-- dup.go --
|
||||
package d1
|
||||
-- dup.go --
|
||||
package d2
|
||||
@@ -0,0 +1,7 @@
|
||||
path=example.com/m
|
||||
version=v1.0.0
|
||||
wantErr=entry "a.go" is both a file and a directory
|
||||
-- a.go --
|
||||
package a
|
||||
-- a.go/b.go --
|
||||
package b
|
||||
@@ -0,0 +1,3 @@
|
||||
path=example.com/empty
|
||||
version=v1.0.0
|
||||
hash=h1:47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
path=example.com/m
|
||||
version=v1.0.0
|
||||
hash=h1:xctQQey8/y7IcBjFZDP/onWLSXhlqcsC3i1fgSdpMHk=
|
||||
-- a.go --
|
||||
package a
|
||||
-- b/GO.MOD --
|
||||
MODULE EXAMPLE.COM/M/B
|
||||
-- b/b.go --
|
||||
package b
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
path=example.com/m
|
||||
version=v1.0.0
|
||||
hash=h1:XduFAgX/GaspZa8Jv4pfzoGEzNaU/r88PiCunijw5ok=
|
||||
-- go.mod --
|
||||
module example.com/m
|
||||
|
||||
go 1.13
|
||||
-- sub/go.mod --
|
||||
module example.com/m/sub
|
||||
-- sub/x.go' --
|
||||
invalid name, but this shouldn't be read
|
||||
@@ -0,0 +1,14 @@
|
||||
path=example.com/m
|
||||
version=v1.0.0
|
||||
hash=h1:5u93LDLN0Me+NGfZtRpA5mHxY8svfykHpq4CMSaBZyc=
|
||||
-- go.mod --
|
||||
module example.com/m
|
||||
|
||||
go 1.13
|
||||
-- vendor/modules.txt --
|
||||
included
|
||||
see comment in isVendoredPackage and golang.org/issue/31562.
|
||||
-- vendor/example.com/x/x.go --
|
||||
excluded
|
||||
-- sub/vendor/sub.txt --
|
||||
excluded
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
path=example.com/m
|
||||
version=v1.0.0
|
||||
wantErr=case-insensitive file name collision: "m.go" and "M.GO"
|
||||
-- m.go --
|
||||
package m
|
||||
-- M.GO --
|
||||
package m
|
||||
@@ -0,0 +1,5 @@
|
||||
path=example.com/m
|
||||
version=v1.0.0
|
||||
hash=h1:Mun5l9cBlDnnV6JasTpio2aZJSbFj++h+814mnKC/OM=
|
||||
-- go.mod/a.go --
|
||||
package a
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
path=ÿ # 0xFF
|
||||
version=v1.0.0
|
||||
wantErr=malformed module path "\xff": invalid UTF-8
|
||||
@@ -0,0 +1,22 @@
|
||||
path=example.com/m
|
||||
version=v1.0.0
|
||||
hash=h1:tpqYOOmuilagXzyqoJ3roUjp8gneQeTv5YVpL6BG7/k=
|
||||
-- go.mod --
|
||||
module example.com/m
|
||||
|
||||
go 1.13
|
||||
-- m.go --
|
||||
package m
|
||||
|
||||
func Foo() int { return 42 }
|
||||
-- cmd/hello/hello.go --
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"example.com/m"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println(m.Foo())
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
path=example.com/m
|
||||
version=v1.0.0
|
||||
wantErr=malformed file path "bad.go'": invalid char '\''
|
||||
-- bad.go' --
|
||||
package bad
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
path=example.com/m
|
||||
version=v1.0.0
|
||||
wantErr=GO.MOD: go.mod files must have lowercase names
|
||||
-- GO.MOD --
|
||||
module example.com/m
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
path=cache
|
||||
version=v1.0.0
|
||||
wantErr=missing dot in first path element
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
path=example.com/m
|
||||
version=v2.0.0
|
||||
wantErr=invalid version: should be v0 or v1, not v2
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
path=example.com/m
|
||||
version=v1.0.0+bad
|
||||
wantErr=version "v1.0.0+bad" is not canonical (should be "v1.0.0")
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user