1// Copyright 2018 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 verify
6
7import (
8	"bytes"
9	"sort"
10	"strings"
11
12	"github.com/golang/dep/gps"
13)
14
15// DeltaDimension defines a bitset enumerating all of the different dimensions
16// along which a Lock, and its constitutent components, can change.
17type DeltaDimension uint32
18
19// Each flag represents an ortohgonal dimension along which Locks can vary with
20// respect to each other.
21const (
22	InputImportsChanged DeltaDimension = 1 << iota
23	ProjectAdded
24	ProjectRemoved
25	SourceChanged
26	VersionChanged
27	RevisionChanged
28	PackagesChanged
29	PruneOptsChanged
30	HashVersionChanged
31	HashChanged
32	AnyChanged = (1 << iota) - 1
33)
34
35// LockDelta represents all possible differences between two Locks.
36type LockDelta struct {
37	AddedImportInputs   []string
38	RemovedImportInputs []string
39	ProjectDeltas       map[gps.ProjectRoot]LockedProjectDelta
40}
41
42// LockedProjectDelta represents all possible state changes of a LockedProject
43// within a Lock. It encapsulates the property-level differences represented by
44// a LockedProjectPropertiesDelta, but can also represent existence deltas - a
45// given name came to exist, or cease to exist, across two Locks.
46type LockedProjectDelta struct {
47	Name                         gps.ProjectRoot
48	ProjectRemoved, ProjectAdded bool
49	LockedProjectPropertiesDelta
50}
51
52// LockedProjectPropertiesDelta represents all possible differences between the
53// properties of two LockedProjects. It can represent deltas for
54// VerifiableProject properties, as well.
55type LockedProjectPropertiesDelta struct {
56	PackagesAdded, PackagesRemoved      []string
57	VersionBefore, VersionAfter         gps.UnpairedVersion
58	RevisionBefore, RevisionAfter       gps.Revision
59	SourceBefore, SourceAfter           string
60	PruneOptsBefore, PruneOptsAfter     gps.PruneOptions
61	HashVersionBefore, HashVersionAfter int
62	HashChanged                         bool
63}
64
65// DiffLocks compares two locks and computes a semantically rich delta between
66// them.
67func DiffLocks(l1, l2 gps.Lock) LockDelta {
68	// Default nil locks to empty locks, so that we can still generate a diff.
69	if l1 == nil {
70		if l2 == nil {
71			// But both locks being nil results in an empty delta.
72			return LockDelta{}
73		}
74		l1 = gps.SimpleLock{}
75	}
76	if l2 == nil {
77		l2 = gps.SimpleLock{}
78	}
79
80	p1, p2 := l1.Projects(), l2.Projects()
81
82	p1 = sortLockedProjects(p1)
83	p2 = sortLockedProjects(p2)
84
85	diff := LockDelta{
86		ProjectDeltas: make(map[gps.ProjectRoot]LockedProjectDelta),
87	}
88
89	var i2next int
90	for i1 := 0; i1 < len(p1); i1++ {
91		lp1 := p1[i1]
92		pr1 := lp1.Ident().ProjectRoot
93
94		lpd := LockedProjectDelta{
95			Name: pr1,
96			// Default to assuming a project was removed, as it will handle both
97			// the obvious removal case (where there's a visible hole in p2),
98			// and the non-obvious case, where p2 is shorter than p1.
99			ProjectRemoved: true,
100		}
101
102		for i2 := i2next; i2 < len(p2); i2++ {
103			lp2 := p2[i2]
104			pr2 := lp2.Ident().ProjectRoot
105
106			switch strings.Compare(string(pr1), string(pr2)) {
107			case 0: // Found a matching project
108				lpd = LockedProjectDelta{
109					Name:                         pr1,
110					LockedProjectPropertiesDelta: DiffLockedProjectProperties(lp1, lp2),
111				}
112				i2next = i2 + 1 // Don't visit this project again
113			case +1: // Found a new project
114				diff.ProjectDeltas[pr2] = LockedProjectDelta{
115					Name:         pr2,
116					ProjectAdded: true,
117				}
118				i2next = i2 + 1 // Don't visit this project again
119				continue        // Keep looking for a matching project
120			}
121
122			break // Done evaluating this project, move onto the next
123		}
124
125		diff.ProjectDeltas[pr1] = lpd
126	}
127
128	// Anything that still hasn't been evaluated are adds
129	for i2 := i2next; i2 < len(p2); i2++ {
130		lp2 := p2[i2]
131		pr2 := lp2.Ident().ProjectRoot
132		diff.ProjectDeltas[pr2] = LockedProjectDelta{
133			Name:         pr2,
134			ProjectAdded: true,
135		}
136	}
137
138	diff.AddedImportInputs, diff.RemovedImportInputs = findAddedAndRemoved(l1.InputImports(), l2.InputImports())
139
140	return diff
141}
142
143func findAddedAndRemoved(l1, l2 []string) (add, remove []string) {
144	// Computing package add/removes might be optimizable to O(n) (?), but it's
145	// not critical path for any known case, so not worth the effort right now.
146	p1, p2 := make(map[string]bool, len(l1)), make(map[string]bool, len(l2))
147
148	for _, pkg := range l1 {
149		p1[pkg] = true
150	}
151	for _, pkg := range l2 {
152		p2[pkg] = true
153	}
154
155	for pkg := range p1 {
156		if !p2[pkg] {
157			remove = append(remove, pkg)
158		}
159	}
160	for pkg := range p2 {
161		if !p1[pkg] {
162			add = append(add, pkg)
163		}
164	}
165
166	return add, remove
167}
168
169// DiffLockedProjectProperties takes two gps.LockedProject and computes a delta
170// for each of their component properties.
171//
172// This function is focused exclusively on the properties of a LockedProject. As
173// such, it does not compare the ProjectRoot part of the LockedProject's
174// ProjectIdentifier, as those are names, and the concern here is a difference
175// in properties, not intrinsic identity.
176func DiffLockedProjectProperties(lp1, lp2 gps.LockedProject) LockedProjectPropertiesDelta {
177	ld := LockedProjectPropertiesDelta{
178		SourceBefore: lp1.Ident().Source,
179		SourceAfter:  lp2.Ident().Source,
180	}
181
182	ld.PackagesAdded, ld.PackagesRemoved = findAddedAndRemoved(lp1.Packages(), lp2.Packages())
183
184	switch v := lp1.Version().(type) {
185	case gps.PairedVersion:
186		ld.VersionBefore, ld.RevisionBefore = v.Unpair(), v.Revision()
187	case gps.Revision:
188		ld.RevisionBefore = v
189	case gps.UnpairedVersion:
190		// This should ideally never happen
191		ld.VersionBefore = v
192	}
193
194	switch v := lp2.Version().(type) {
195	case gps.PairedVersion:
196		ld.VersionAfter, ld.RevisionAfter = v.Unpair(), v.Revision()
197	case gps.Revision:
198		ld.RevisionAfter = v
199	case gps.UnpairedVersion:
200		// This should ideally never happen
201		ld.VersionAfter = v
202	}
203
204	vp1, ok1 := lp1.(VerifiableProject)
205	vp2, ok2 := lp2.(VerifiableProject)
206
207	if ok1 && ok2 {
208		ld.PruneOptsBefore, ld.PruneOptsAfter = vp1.PruneOpts, vp2.PruneOpts
209		ld.HashVersionBefore, ld.HashVersionAfter = vp1.Digest.HashVersion, vp2.Digest.HashVersion
210
211		if !bytes.Equal(vp1.Digest.Digest, vp2.Digest.Digest) {
212			ld.HashChanged = true
213		}
214	} else if ok1 {
215		ld.PruneOptsBefore = vp1.PruneOpts
216		ld.HashVersionBefore = vp1.Digest.HashVersion
217		ld.HashChanged = true
218	} else if ok2 {
219		ld.PruneOptsAfter = vp2.PruneOpts
220		ld.HashVersionAfter = vp2.Digest.HashVersion
221		ld.HashChanged = true
222	}
223
224	return ld
225}
226
227// Changed indicates whether the delta contains a change along the dimensions
228// with their corresponding bits set.
229//
230// This implementation checks the topmost-level Lock properties
231func (ld LockDelta) Changed(dims DeltaDimension) bool {
232	if dims&InputImportsChanged != 0 && (len(ld.AddedImportInputs) > 0 || len(ld.RemovedImportInputs) > 0) {
233		return true
234	}
235
236	for _, ld := range ld.ProjectDeltas {
237		if ld.Changed(dims & ^InputImportsChanged) {
238			return true
239		}
240	}
241
242	return false
243}
244
245// Changes returns a bitset indicating the dimensions along which deltas exist across
246// all contents of the LockDelta.
247//
248// This recurses down into the individual LockedProjectDeltas contained within
249// the LockDelta. A single delta along a particular dimension from a single
250// project is sufficient to flip the bit on for that dimension.
251func (ld LockDelta) Changes() DeltaDimension {
252	var dd DeltaDimension
253	if len(ld.AddedImportInputs) > 0 || len(ld.RemovedImportInputs) > 0 {
254		dd |= InputImportsChanged
255	}
256
257	for _, ld := range ld.ProjectDeltas {
258		dd |= ld.Changes()
259	}
260
261	return dd
262}
263
264// Changed indicates whether the delta contains a change along the dimensions
265// with their corresponding bits set.
266//
267// For example, if only the Revision changed, and this method is called with
268// SourceChanged | VersionChanged, it will return false; if it is called with
269// VersionChanged | RevisionChanged, it will return true.
270func (ld LockedProjectDelta) Changed(dims DeltaDimension) bool {
271	if dims&ProjectAdded != 0 && ld.WasAdded() {
272		return true
273	}
274
275	if dims&ProjectRemoved != 0 && ld.WasRemoved() {
276		return true
277	}
278
279	return ld.LockedProjectPropertiesDelta.Changed(dims & ^ProjectAdded & ^ProjectRemoved)
280}
281
282// Changes returns a bitset indicating the dimensions along which there were
283// changes between the compared LockedProjects. This includes both
284// existence-level deltas (add/remove) and property-level deltas.
285func (ld LockedProjectDelta) Changes() DeltaDimension {
286	var dd DeltaDimension
287	if ld.WasAdded() {
288		dd |= ProjectAdded
289	}
290
291	if ld.WasRemoved() {
292		dd |= ProjectRemoved
293	}
294
295	return dd | ld.LockedProjectPropertiesDelta.Changes()
296}
297
298// WasRemoved returns true if the named project existed in the first lock, but
299// did not exist in the second lock.
300func (ld LockedProjectDelta) WasRemoved() bool {
301	return ld.ProjectRemoved
302}
303
304// WasAdded returns true if the named project did not exist in the first lock,
305// but did exist in the second lock.
306func (ld LockedProjectDelta) WasAdded() bool {
307	return ld.ProjectAdded
308}
309
310// Changed indicates whether the delta contains a change along the dimensions
311// with their corresponding bits set.
312//
313// For example, if only the Revision changed, and this method is called with
314// SourceChanged | VersionChanged, it will return false; if it is called with
315// VersionChanged | RevisionChanged, it will return true.
316func (ld LockedProjectPropertiesDelta) Changed(dims DeltaDimension) bool {
317	if dims&SourceChanged != 0 && ld.SourceChanged() {
318		return true
319	}
320	if dims&RevisionChanged != 0 && ld.RevisionChanged() {
321		return true
322	}
323	if dims&PruneOptsChanged != 0 && ld.PruneOptsChanged() {
324		return true
325	}
326	if dims&HashChanged != 0 && ld.HashChanged {
327		return true
328	}
329	if dims&HashVersionChanged != 0 && ld.HashVersionChanged() {
330		return true
331	}
332	if dims&VersionChanged != 0 && ld.VersionChanged() {
333		return true
334	}
335	if dims&PackagesChanged != 0 && ld.PackagesChanged() {
336		return true
337	}
338
339	return false
340}
341
342// Changes returns a bitset indicating the dimensions along which there were
343// changes between the compared LockedProjects.
344func (ld LockedProjectPropertiesDelta) Changes() DeltaDimension {
345	var dd DeltaDimension
346	if ld.SourceChanged() {
347		dd |= SourceChanged
348	}
349	if ld.RevisionChanged() {
350		dd |= RevisionChanged
351	}
352	if ld.PruneOptsChanged() {
353		dd |= PruneOptsChanged
354	}
355	if ld.HashChanged {
356		dd |= HashChanged
357	}
358	if ld.HashVersionChanged() {
359		dd |= HashVersionChanged
360	}
361	if ld.VersionChanged() {
362		dd |= VersionChanged
363	}
364	if ld.PackagesChanged() {
365		dd |= PackagesChanged
366	}
367
368	return dd
369}
370
371// SourceChanged returns true if the source field differed between the first and
372// second locks.
373func (ld LockedProjectPropertiesDelta) SourceChanged() bool {
374	return ld.SourceBefore != ld.SourceAfter
375}
376
377// VersionChanged returns true if the version property differed between the
378// first and second locks. In addition to simple changes (e.g. 1.0.1 -> 1.0.2),
379// this also includes all possible version type changes either going from a
380// paired version to a plain revision, or the reverse direction, or the type of
381// unpaired version changing (e.g. branch -> semver).
382func (ld LockedProjectPropertiesDelta) VersionChanged() bool {
383	if ld.VersionBefore == nil && ld.VersionAfter == nil {
384		return false
385	} else if (ld.VersionBefore == nil || ld.VersionAfter == nil) || (ld.VersionBefore.Type() != ld.VersionAfter.Type()) {
386		return true
387	} else if !ld.VersionBefore.Matches(ld.VersionAfter) {
388		return true
389	}
390
391	return false
392}
393
394// RevisionChanged returns true if the revision property differed between the
395// first and second locks.
396func (ld LockedProjectPropertiesDelta) RevisionChanged() bool {
397	return ld.RevisionBefore != ld.RevisionAfter
398}
399
400// PackagesChanged returns true if the package set gained or lost members (or
401// both) between the first and second locks.
402func (ld LockedProjectPropertiesDelta) PackagesChanged() bool {
403	return len(ld.PackagesAdded) > 0 || len(ld.PackagesRemoved) > 0
404}
405
406// PruneOptsChanged returns true if the pruning flags for the project changed
407// between the first and second locks.
408func (ld LockedProjectPropertiesDelta) PruneOptsChanged() bool {
409	return ld.PruneOptsBefore != ld.PruneOptsAfter
410}
411
412// HashVersionChanged returns true if the version of the hashing algorithm
413// changed between the first and second locks.
414func (ld LockedProjectPropertiesDelta) HashVersionChanged() bool {
415	return ld.HashVersionBefore != ld.HashVersionAfter
416}
417
418// HashVersionWasZero returns true if the first lock had a zero hash version,
419// which can only mean it was uninitialized.
420func (ld LockedProjectPropertiesDelta) HashVersionWasZero() bool {
421	return ld.HashVersionBefore == 0
422}
423
424// sortLockedProjects returns a sorted copy of lps, or itself if already sorted.
425func sortLockedProjects(lps []gps.LockedProject) []gps.LockedProject {
426	if len(lps) <= 1 || sort.SliceIsSorted(lps, func(i, j int) bool {
427		return lps[i].Ident().Less(lps[j].Ident())
428	}) {
429		return lps
430	}
431
432	cp := make([]gps.LockedProject, len(lps))
433	copy(cp, lps)
434
435	sort.Slice(cp, func(i, j int) bool {
436		return cp[i].Ident().Less(cp[j].Ident())
437	})
438	return cp
439}
440