1// Copyright 2016 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 dep
6
7import (
8	"bytes"
9	"io"
10	"sort"
11
12	"github.com/golang/dep/gps"
13	"github.com/golang/dep/gps/verify"
14	"github.com/pelletier/go-toml"
15	"github.com/pkg/errors"
16)
17
18// LockName is the lock file name used by dep.
19const LockName = "Gopkg.lock"
20
21// Lock holds lock file data and implements gps.Lock.
22type Lock struct {
23	SolveMeta SolveMeta
24	P         []gps.LockedProject
25}
26
27// SolveMeta holds metadata about the solving process that created the lock that
28// is not specific to any individual project.
29type SolveMeta struct {
30	AnalyzerName    string
31	AnalyzerVersion int
32	SolverName      string
33	SolverVersion   int
34	InputImports    []string
35}
36
37type rawLock struct {
38	SolveMeta solveMeta          `toml:"solve-meta"`
39	Projects  []rawLockedProject `toml:"projects"`
40}
41
42type solveMeta struct {
43	AnalyzerName    string   `toml:"analyzer-name"`
44	AnalyzerVersion int      `toml:"analyzer-version"`
45	SolverName      string   `toml:"solver-name"`
46	SolverVersion   int      `toml:"solver-version"`
47	InputImports    []string `toml:"input-imports"`
48}
49
50type rawLockedProject struct {
51	Name      string   `toml:"name"`
52	Branch    string   `toml:"branch,omitempty"`
53	Revision  string   `toml:"revision"`
54	Version   string   `toml:"version,omitempty"`
55	Source    string   `toml:"source,omitempty"`
56	Packages  []string `toml:"packages"`
57	PruneOpts string   `toml:"pruneopts"`
58	Digest    string   `toml:"digest"`
59}
60
61func readLock(r io.Reader) (*Lock, error) {
62	buf := &bytes.Buffer{}
63	_, err := buf.ReadFrom(r)
64	if err != nil {
65		return nil, errors.Wrap(err, "Unable to read byte stream")
66	}
67
68	raw := rawLock{}
69	err = toml.Unmarshal(buf.Bytes(), &raw)
70	if err != nil {
71		return nil, errors.Wrap(err, "Unable to parse the lock as TOML")
72	}
73
74	return fromRawLock(raw)
75}
76
77func fromRawLock(raw rawLock) (*Lock, error) {
78	l := &Lock{
79		P: make([]gps.LockedProject, 0, len(raw.Projects)),
80	}
81
82	l.SolveMeta.AnalyzerName = raw.SolveMeta.AnalyzerName
83	l.SolveMeta.AnalyzerVersion = raw.SolveMeta.AnalyzerVersion
84	l.SolveMeta.SolverName = raw.SolveMeta.SolverName
85	l.SolveMeta.SolverVersion = raw.SolveMeta.SolverVersion
86	l.SolveMeta.InputImports = raw.SolveMeta.InputImports
87
88	for _, ld := range raw.Projects {
89		r := gps.Revision(ld.Revision)
90
91		var v gps.Version = r
92		if ld.Version != "" {
93			if ld.Branch != "" {
94				return nil, errors.Errorf("lock file specified both a branch (%s) and version (%s) for %s", ld.Branch, ld.Version, ld.Name)
95			}
96			v = gps.NewVersion(ld.Version).Pair(r)
97		} else if ld.Branch != "" {
98			v = gps.NewBranch(ld.Branch).Pair(r)
99		} else if r == "" {
100			return nil, errors.Errorf("lock file has entry for %s, but specifies no branch or version", ld.Name)
101		}
102
103		id := gps.ProjectIdentifier{
104			ProjectRoot: gps.ProjectRoot(ld.Name),
105			Source:      ld.Source,
106		}
107
108		var err error
109		vp := verify.VerifiableProject{
110			LockedProject: gps.NewLockedProject(id, v, ld.Packages),
111		}
112		if ld.Digest != "" {
113			vp.Digest, err = verify.ParseVersionedDigest(ld.Digest)
114			if err != nil {
115				return nil, err
116			}
117		}
118
119		po, err := gps.ParsePruneOptions(ld.PruneOpts)
120		if err != nil {
121			return nil, errors.Errorf("%s in prune options for %s", err.Error(), ld.Name)
122		}
123		// Add the vendor pruning bit so that gps doesn't get confused
124		vp.PruneOpts = po | gps.PruneNestedVendorDirs
125
126		l.P = append(l.P, vp)
127	}
128
129	return l, nil
130}
131
132// Projects returns the list of LockedProjects contained in the lock data.
133func (l *Lock) Projects() []gps.LockedProject {
134	if l == nil || l == (*Lock)(nil) {
135		return nil
136	}
137	return l.P
138}
139
140// InputImports reports the list of input imports that were used in generating
141// this Lock.
142func (l *Lock) InputImports() []string {
143	if l == nil || l == (*Lock)(nil) {
144		return nil
145	}
146	return l.SolveMeta.InputImports
147}
148
149// HasProjectWithRoot checks if the lock contains a project with the provided
150// ProjectRoot.
151//
152// This check is O(n) in the number of projects.
153func (l *Lock) HasProjectWithRoot(root gps.ProjectRoot) bool {
154	for _, p := range l.P {
155		if p.Ident().ProjectRoot == root {
156			return true
157		}
158	}
159
160	return false
161}
162
163func (l *Lock) dup() *Lock {
164	l2 := &Lock{
165		SolveMeta: l.SolveMeta,
166		P:         make([]gps.LockedProject, len(l.P)),
167	}
168
169	l2.SolveMeta.InputImports = make([]string, len(l.SolveMeta.InputImports))
170	copy(l2.SolveMeta.InputImports, l.SolveMeta.InputImports)
171	copy(l2.P, l.P)
172
173	return l2
174}
175
176// toRaw converts the manifest into a representation suitable to write to the lock file
177func (l *Lock) toRaw() rawLock {
178	raw := rawLock{
179		SolveMeta: solveMeta{
180			AnalyzerName:    l.SolveMeta.AnalyzerName,
181			AnalyzerVersion: l.SolveMeta.AnalyzerVersion,
182			InputImports:    l.SolveMeta.InputImports,
183			SolverName:      l.SolveMeta.SolverName,
184			SolverVersion:   l.SolveMeta.SolverVersion,
185		},
186		Projects: make([]rawLockedProject, 0, len(l.P)),
187	}
188
189	sort.Slice(l.P, func(i, j int) bool {
190		return l.P[i].Ident().Less(l.P[j].Ident())
191	})
192
193	for _, lp := range l.P {
194		id := lp.Ident()
195		ld := rawLockedProject{
196			Name:     string(id.ProjectRoot),
197			Source:   id.Source,
198			Packages: lp.Packages(),
199		}
200
201		v := lp.Version()
202		ld.Revision, ld.Branch, ld.Version = gps.VersionComponentStrings(v)
203
204		// This will panic if the lock isn't the expected dynamic type. We can
205		// relax this later if it turns out to create real problems, but there's
206		// no intended case in which this is untrue, so it's preferable to start
207		// by failing hard if those expectations aren't met.
208		vp := lp.(verify.VerifiableProject)
209		ld.Digest = vp.Digest.String()
210		ld.PruneOpts = (vp.PruneOpts & ^gps.PruneNestedVendorDirs).String()
211
212		raw.Projects = append(raw.Projects, ld)
213	}
214
215	return raw
216}
217
218// MarshalTOML serializes this lock into TOML via an intermediate raw form.
219func (l *Lock) MarshalTOML() ([]byte, error) {
220	raw := l.toRaw()
221	var buf bytes.Buffer
222	enc := toml.NewEncoder(&buf).ArraysWithOneElementPerLine(true)
223	err := enc.Encode(raw)
224	return buf.Bytes(), errors.Wrap(err, "Unable to marshal lock to TOML string")
225}
226
227// LockFromSolution converts a gps.Solution to dep's representation of a lock.
228// It makes sure that that the provided prune options are set correctly, as the
229// solver does not use VerifiableProjects for new selections it makes.
230//
231// Data is defensively copied wherever necessary to ensure the resulting *Lock
232// shares no memory with the input solution.
233func LockFromSolution(in gps.Solution, prune gps.CascadingPruneOptions) *Lock {
234	p := in.Projects()
235
236	l := &Lock{
237		SolveMeta: SolveMeta{
238			AnalyzerName:    in.AnalyzerName(),
239			AnalyzerVersion: in.AnalyzerVersion(),
240			InputImports:    in.InputImports(),
241			SolverName:      in.SolverName(),
242			SolverVersion:   in.SolverVersion(),
243		},
244		P: make([]gps.LockedProject, 0, len(p)),
245	}
246
247	for _, lp := range p {
248		if vp, ok := lp.(verify.VerifiableProject); ok {
249			l.P = append(l.P, vp)
250		} else {
251			l.P = append(l.P, verify.VerifiableProject{
252				LockedProject: lp,
253				PruneOpts:     prune.PruneOptionsFor(lp.Ident().ProjectRoot),
254			})
255		}
256	}
257
258	return l
259}
260