1// Copyright 2019 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package sumdb
6
7import (
8	"context"
9	"fmt"
10	"sync"
11
12	"golang.org/x/mod/module"
13	"golang.org/x/mod/sumdb/note"
14	"golang.org/x/mod/sumdb/tlog"
15)
16
17// NewTestServer constructs a new [TestServer]
18// that will sign its tree with the given signer key
19// (see [golang.org/x/mod/sumdb/note])
20// and fetch new records as needed by calling gosum.
21func NewTestServer(signer string, gosum func(path, vers string) ([]byte, error)) *TestServer {
22	return &TestServer{signer: signer, gosum: gosum}
23}
24
25// A TestServer is an in-memory implementation of [ServerOps] for testing.
26type TestServer struct {
27	signer string
28	gosum  func(path, vers string) ([]byte, error)
29
30	mu      sync.Mutex
31	hashes  testHashes
32	records [][]byte
33	lookup  map[string]int64
34}
35
36// testHashes implements tlog.HashReader, reading from a slice.
37type testHashes []tlog.Hash
38
39func (h testHashes) ReadHashes(indexes []int64) ([]tlog.Hash, error) {
40	var list []tlog.Hash
41	for _, id := range indexes {
42		list = append(list, h[id])
43	}
44	return list, nil
45}
46
47func (s *TestServer) Signed(ctx context.Context) ([]byte, error) {
48	s.mu.Lock()
49	defer s.mu.Unlock()
50
51	size := int64(len(s.records))
52	h, err := tlog.TreeHash(size, s.hashes)
53	if err != nil {
54		return nil, err
55	}
56	text := tlog.FormatTree(tlog.Tree{N: size, Hash: h})
57	signer, err := note.NewSigner(s.signer)
58	if err != nil {
59		return nil, err
60	}
61	return note.Sign(&note.Note{Text: string(text)}, signer)
62}
63
64func (s *TestServer) ReadRecords(ctx context.Context, id, n int64) ([][]byte, error) {
65	s.mu.Lock()
66	defer s.mu.Unlock()
67
68	var list [][]byte
69	for i := int64(0); i < n; i++ {
70		if id+i >= int64(len(s.records)) {
71			return nil, fmt.Errorf("missing records")
72		}
73		list = append(list, s.records[id+i])
74	}
75	return list, nil
76}
77
78func (s *TestServer) Lookup(ctx context.Context, m module.Version) (int64, error) {
79	key := m.String()
80	s.mu.Lock()
81	id, ok := s.lookup[key]
82	s.mu.Unlock()
83	if ok {
84		return id, nil
85	}
86
87	// Look up module and compute go.sum lines.
88	data, err := s.gosum(m.Path, m.Version)
89	if err != nil {
90		return 0, err
91	}
92
93	s.mu.Lock()
94	defer s.mu.Unlock()
95
96	// We ran the fetch without the lock.
97	// If another fetch happened and committed, use it instead.
98	id, ok = s.lookup[key]
99	if ok {
100		return id, nil
101	}
102
103	// Add record.
104	id = int64(len(s.records))
105	s.records = append(s.records, data)
106	if s.lookup == nil {
107		s.lookup = make(map[string]int64)
108	}
109	s.lookup[key] = id
110	hashes, err := tlog.StoredHashesForRecordHash(id, tlog.RecordHash(data), s.hashes)
111	if err != nil {
112		panic(err)
113	}
114	s.hashes = append(s.hashes, hashes...)
115
116	return id, nil
117}
118
119func (s *TestServer) ReadTileData(ctx context.Context, t tlog.Tile) ([]byte, error) {
120	s.mu.Lock()
121	defer s.mu.Unlock()
122
123	return tlog.ReadTileData(t, s.hashes)
124}
125