/
identifier.go
821 lines (703 loc) · 23.9 KB
/
identifier.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
// SPDX-License-Identifier: Apache-2.0
package identifier
import (
"fmt"
"io/fs"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
"sync"
"github.com/CycloneDX/sbom-utility/log"
"golang.org/x/exp/slices"
"golang.org/x/sync/errgroup"
"github.com/CycloneDX/license-scanner/licenses"
"github.com/CycloneDX/license-scanner/normalizer"
)
var (
Logger = log.NewLogger(log.INFO)
nonAlphaRE = regexp.MustCompile(`^[^A-Za-z0-9]*$`)
)
type Options struct {
ForceResult bool
OmitBlocks bool
Enhancements Enhancements
}
type licenseMatch struct {
LicenseId string
Match Match
}
type Match struct {
Begins int
Ends int
}
type PatternMatch struct {
Text string
Begins int
Ends int
}
type IdentifierResults struct {
Matches map[string][]Match
Blocks []Block
File string
OriginalText string
NormalizedText string
Hash normalizer.Digest
Notes string
AcceptablePatternMatches []PatternMatch
KeywordMatches []PatternMatch
CopyRightStatements []PatternMatch
}
type Block struct {
Text string
Matches []string
}
func Identify(options Options, licenseLibrary *licenses.LicenseLibrary, normalizedData normalizer.NormalizationData) (IdentifierResults, error) {
// find the licenses in the normalized text and return a list of SPDX IDs
// in case of an error, return as much as we have along with an error
licenseResults, err := findAllLicensesInNormalizedData(licenseLibrary, normalizedData)
if err != nil {
return IdentifierResults{}, err
}
if err := FromOptions(&licenseResults, options.Enhancements, licenseLibrary); err != nil {
return IdentifierResults{}, err
}
if err := applyMutatorLicenses(licenseLibrary.LicenseMap, &licenseResults); err != nil {
return IdentifierResults{}, err
}
if options.OmitBlocks {
licenseResults.Blocks = []Block{}
}
return licenseResults, err
}
func IdentifyLicensesInString(input string, options Options, licenseLibrary *licenses.LicenseLibrary) (IdentifierResults, error) {
// instantiate normalizedData with the input license text
normalizedData := normalizer.NormalizationData{
OriginalText: input,
}
// normalize the input license text
if err := normalizedData.NormalizeText(); err != nil {
return IdentifierResults{}, err
}
return Identify(options, licenseLibrary, normalizedData)
}
func IdentifyLicensesInFile(filePath string, options Options, licenseLibrary *licenses.LicenseLibrary) (IdentifierResults, error) {
fi, err := os.Stat(filePath)
if err != nil {
return IdentifierResults{}, err
}
if fi.Size() > 1000000 {
Logger.Errorf("file too large (%v > 1000000)", fi.Size()) // log error, but return nil
return IdentifierResults{}, nil
}
b, err := ioutil.ReadFile(filePath)
if err != nil {
return IdentifierResults{}, err
}
input := string(b)
result, err := IdentifyLicensesInString(input, options, licenseLibrary)
result.File = filePath
return result, err
}
func IdentifyLicensesInDirectory(dirPath string, options Options, licenseLibrary *licenses.LicenseLibrary) (ret []IdentifierResults, err error) {
var lfs []string
if err := filepath.WalkDir(dirPath, func(path string, d fs.DirEntry, err error) error {
if err != nil {
fmt.Printf("prevent panic by handling failure accessing a path %q: %v\n", path, err)
return err
}
if !d.IsDir() {
info, _ := d.Info()
if info.Size() > 0 {
lfs = append(lfs, path)
}
}
return nil
}); err != nil {
fmt.Printf("error walking the path %v: %v\n", dirPath, err)
return nil, err
}
// errGroup to do the work in parallel until error
workers := errgroup.Group{}
workers.SetLimit(10)
ch := make(chan IdentifierResults, 10)
// WaitGroup to know when we have all the results
waitForResults := sync.WaitGroup{}
waitForResults.Add(1)
// Start receiving the results until channel closes
go func() {
for ir := range ch {
ret = append(ret, ir)
}
waitForResults.Done()
}()
// Loop using a worker to send results to a channel
for _, lf := range lfs {
lf := lf
workers.Go(func() error {
ir, err := IdentifyLicensesInFile(lf, options, licenseLibrary)
if err == nil {
ch <- ir
}
return err
})
}
// Close the channel when done or error
go func() {
err = workers.Wait()
close(ch)
}()
// Make sure we got all the results
waitForResults.Wait()
return ret, err
}
func findAllLicensesInNormalizedData(licenseLibrary *licenses.LicenseLibrary, normalizedData normalizer.NormalizationData) (IdentifierResults, error) {
// initialize the result with original license text, normalized license text, and hash (md5, sha256, and sha512)
ret := IdentifierResults{
OriginalText: normalizedData.OriginalText,
NormalizedText: normalizedData.NormalizedText,
Hash: normalizedData.Hash,
}
// LicenseID-to-matches map to return
ret.Matches = make(map[string][]Match)
// List with LicenseID and indexes for generating text blocks
var licensesMatched []licenseMatch
for id, lic := range licenseLibrary.LicenseMap {
matches, err := findLicenseInNormalizedData(lic, normalizedData, licenseLibrary)
if err != nil {
return ret, err
}
// Sort the matches slice by start and end index.
sort.Slice(matches, func(i, j int) bool {
if matches[i].Begins != matches[j].Begins {
return matches[i].Begins < matches[j].Begins
} else {
return matches[i].Ends < matches[j].Ends
}
})
for i := range matches {
if i > 0 && matches[i] == matches[i-1] {
continue // remove duplicates
}
licensesMatched = append(licensesMatched, licenseMatch{LicenseId: id, Match: matches[i]})
ret.Matches[id] = append(ret.Matches[id], matches[i])
}
}
// Generate Blocks.
blocks, err := generateTextBlocks(normalizedData.OriginalText, licensesMatched)
if err != nil {
return ret, err
}
ret.Blocks = blocks
return ret, nil
}
func findLicenseInNormalizedData(lic licenses.License, normalizedData normalizer.NormalizationData, ll *licenses.LicenseLibrary) (licenseMatches []Match, err error) {
// TODO: If we are not using the match blocks, etc, then do the faster alias checks first.
// Get the license pattern matches.
licenseMatches, err = findPatterns(lic.PrimaryPatterns, normalizedData, licenseMatches, ll)
if err != nil {
return licenseMatches, err
}
// If we don't already have a more interesting match, then see if there is an alias hit
if len(licenseMatches) == 0 {
licenseMatches = findAnyAlias(lic.Aliases, normalizedData, licenseMatches)
}
// If we don't already have a more interesting match, then see if there is a URL hit
if len(licenseMatches) == 0 {
licenseMatches = findAnyURL(lic.URLs, normalizedData, licenseMatches)
}
// If there were no results, return null.
if len(licenseMatches) == 0 {
return nil, nil
}
// If there are associated patterns, check those.
return findPatterns(lic.AssociatedPatterns, normalizedData, licenseMatches, ll)
}
// findAny finds one matching string which meets word boundary conditions (and url conditions)
func findAny(ss []string, normalized normalizer.NormalizationData, isURL bool, licenseMatches []Match) []Match {
for _, s := range ss {
next := 0
for i := strings.Index(normalized.NormalizedText, s); i > -1; i = strings.Index(normalized.NormalizedText[next:], s) {
i = next + i // position in the full normalized text string
next = i + 1 // if we continue to loop, start one char after the last hit
begin, end, found := findBoundaries(i, s, normalized, isURL)
if found {
return appendIndexMappedMatch(begin, end, normalized, licenseMatches)
}
}
}
return licenseMatches
}
func findBoundaries(start int, s string, nd normalizer.NormalizationData, isURL bool) (begin int, end int, ok bool) {
begin, ok = findBeginBoundary(start, nd, isURL)
if !ok {
return -1, -1, false
}
end, ok = findEndBoundary(start, s, nd, isURL)
if !ok {
return -1, -1, false
}
return begin, end, true
}
func findBeginBoundary(start int, nd normalizer.NormalizationData, isURL bool) (begin int, ok bool) {
// Starting at position zero is always an ok boundary
if start == 0 {
return 0, true
}
begin = start
// First scan to include URL prefixes https?://(www.)?
if isURL {
begin = includeURLPrefix(begin, nd)
if begin == 0 {
return 0, true // position 0 boundary
}
}
begin -= 1
if begin == 0 {
return 0, true
}
c := nd.NormalizedText[begin]
if c == '(' {
begin -= 1
if begin == 0 {
return 0, true
}
c = nd.NormalizedText[begin]
}
switch { // Allows anything except a-z0-9
case c >= 'a' && c <= 'z':
return -1, false
case c >= '0' && c <= '9':
return -1, false
}
return begin, true // Space-paren word boundary
}
func findEndBoundary(start int, s string, nd normalizer.NormalizationData, isURL bool) (end int, ok bool) {
end = start + len(s)
max := len(nd.NormalizedText)
if end >= max {
return end, true // end of string is ok boundary
}
// if isURL then take additional suffix fragments like URL/alphas/digits/dots/dashes-under_scores/ as part of the URL
if isURL {
for ; end < max; end++ {
c := nd.NormalizedText[end]
switch {
case c >= 'a' && c <= 'z':
continue
case c >= '0' && c <= '9':
continue
}
switch c {
case '.', '-', '_', '/':
continue
}
break // break loop when we stop finding a-z0-9_-./
}
}
if end < max && nd.NormalizedText[end] == ')' {
end += 1 // include the end parens
}
// Need word boundary if not the very end of the normalized text
if end < max {
c := nd.NormalizedText[end]
switch { // Allows anything except a-z0-9
case c >= 'a' && c <= 'z':
return -1, false
case c >= '0' && c <= '9':
return -1, false
}
}
return end, true // found an ok boundary
}
func includeURLPrefix(begin int, nd normalizer.NormalizationData) int {
wwwDot := "www."
length := len(wwwDot)
if begin >= length && wwwDot == nd.NormalizedText[begin-length:begin] {
// Add the optional www. prefix to the match
begin = begin - length
}
httpSlashSlash := "http://" // normalizer drops the 's', URLs are cut at ://
length = len(httpSlashSlash)
if begin >= length && httpSlashSlash == nd.NormalizedText[begin-length:begin] {
// Add the optional http:// prefix to the match
begin = begin - length
}
return begin
}
func appendIndexMappedMatch(begin int, end int, normalizedData normalizer.NormalizationData, licenseMatches []Match) []Match {
indexMapLen := len(normalizedData.IndexMap)
if end < indexMapLen {
return append(licenseMatches, Match{Begins: normalizedData.IndexMap[begin], Ends: normalizedData.IndexMap[end]})
} else {
// End of map is out of range, so use the last index in the map
return append(licenseMatches, Match{Begins: normalizedData.IndexMap[begin], Ends: normalizedData.IndexMap[indexMapLen-1]})
}
}
func findAnyAlias(urls []string, normalized normalizer.NormalizationData, licenseMatches []Match) []Match {
return findAny(urls, normalized, false, licenseMatches)
}
func findAnyURL(urls []string, normalized normalizer.NormalizationData, licenseMatches []Match) []Match {
return findAny(urls, normalized, true, licenseMatches)
}
func findPatterns(patterns []*licenses.PrimaryPatterns, normalizedData normalizer.NormalizationData, licenseMatches []Match, ll *licenses.LicenseLibrary) ([]Match, error) {
// errGroup to do the work in parallel until error
workers := errgroup.Group{}
workers.SetLimit(10)
ch := make(chan []Match, 10)
// WaitGroup to know when we have all the results
waitForResults := sync.WaitGroup{}
waitForResults.Add(1)
// Start receiving the results until channel closes
go func() {
for patternMatches := range ch {
if len(patternMatches) > 0 {
licenseMatches = append(licenseMatches, patternMatches...)
}
}
waitForResults.Done()
}()
// Loop with the slow part using a worker to send results to a channel
for _, pattern := range patterns {
ppk := licenses.LicensePatternKey{
FilePath: pattern.FileName,
}
preChecksRequired := ll.PrimaryPatternPreCheckMap[ppk]
if preChecksRequired != nil && !PassedStaticBlocksChecks(preChecksRequired.StaticBlocks, normalizedData) {
continue
}
p := pattern
nD := normalizedData
workers.Go(func() error {
patternMatches, err := FindMatchingPatternInNormalizedData(p, nD)
if err == nil {
ch <- patternMatches
}
return err
})
}
// Close the channel when done or error
var err error
go func() {
err = workers.Wait()
close(ch)
}()
// Make sure we got all the results
waitForResults.Wait()
return licenseMatches, err
}
func FindMatchingPatternInNormalizedData(matchingPattern *licenses.PrimaryPatterns, normalized normalizer.NormalizationData) (results []Match, err error) {
re, err := licenses.GenerateMatchingPatternFromSourceText(matchingPattern)
if err != nil || re == nil {
return results, err
}
matches := re.FindAllStringIndex(normalized.NormalizedText, -1)
for _, match := range matches {
// Create the result object, with the start and end points in the original text.
if match[1] < len(normalized.IndexMap) {
results = append(results, Match{Begins: normalized.IndexMap[match[0]], Ends: normalized.IndexMap[match[1]-1]})
} else {
// End of map is out of range, so use the last index in the map
results = append(results, Match{Begins: normalized.IndexMap[match[0]], Ends: normalized.IndexMap[len(normalized.IndexMap)-1]})
}
}
return results, err
}
// PassedStaticBlocksChecks verifies static blocks are present, if any
func PassedStaticBlocksChecks(staticBlocks []string, nd normalizer.NormalizationData) bool {
for i := range staticBlocks {
// If the input does not contain a static block, stop immediately and return false.
if !strings.Contains(nd.NormalizedText, staticBlocks[i]) {
return false
}
}
return true
}
func generateTextBlocks(originalText string, matches []licenseMatch) ([]Block, error) {
// If there were no license results or licenses found, return with a single block.
if len(matches) == 0 {
return []Block{{Text: originalText}}, nil
}
var blocks []Block
lastEnd := 0
for _, nextMatch := range matches {
// Create the block for everything leading up to the new match.
if lastEnd < nextMatch.Match.Begins {
blocks = appendNewBlock(blocks, originalText[lastEnd:nextMatch.Match.Begins], "")
lastEnd = nextMatch.Match.Begins
}
begin := nextMatch.Match.Begins
if begin < lastEnd {
begin = lastEnd
}
nextEnd := nextMatch.Match.Ends + 1
if nextEnd > lastEnd {
if nextEnd > len(originalText) {
blocks = appendNewBlock(blocks, originalText[begin:], nextMatch.LicenseId)
} else {
blocks = appendNewBlock(blocks, originalText[begin:nextEnd], nextMatch.LicenseId)
}
lastEnd = nextEnd
}
}
if lastEnd < len(originalText) {
blocks = appendNewBlock(blocks, originalText[lastEnd:], "")
}
return blocks, nil
}
func appendNewBlock(blocks []Block, newBlockText string, licenseId string) []Block {
numBlocks := len(blocks)
if numBlocks > 0 {
prevBlock := &blocks[numBlocks-1]
if len(prevBlock.Matches) == 1 && prevBlock.Matches[0] == licenseId {
// Blocks which are functionally identical to the previous block should also be appended.
prevBlock.Text += newBlockText
return blocks
} else if licenseId == "" && nonAlphaRE.MatchString(newBlockText) {
// Unmatched blocks containing no alphanumeric text, should be appended to the previous block.
prevBlock.Text += newBlockText
return blocks
}
}
var newBlock Block
newBlock.Text = newBlockText
if licenseId != "" {
newBlock.Matches = []string{licenseId}
}
blocks = append(blocks, newBlock)
return blocks
}
func applyMutatorLicenses(allLicenses licenses.LicenseMap, licenseResults *IdentifierResults) error {
var previousLicenses []licenses.License
var previousMutators []licenses.License
var affectedBlocks []Block
appliedMutation := false
// Iterate over all blocks.
for _, b := range licenseResults.Blocks {
// Previous state information:
currentLicenses := previousLicenses
currentMutators := previousMutators
var newLicenses []licenses.License
var newMutators []licenses.License
// TODO: // Ignore any Blocks which are not relevant to licenses.
// TODO: check for a bunch of things to ignore
if len(b.Matches) == 0 {
// Apply mutators to previous state.
// TODO: This invalid state should be refactored away
if len(previousLicenses) > 1 {
return fmt.Errorf("Invalid state. Should be only one previous license.")
}
if len(previousLicenses) > 0 {
appliedMutation = appliedMutation || applyMutatorsInAffectedBlocks(affectedBlocks, previousLicenses[0], previousMutators)
}
// Set up new state.
previousLicenses, previousMutators, affectedBlocks = nil, nil, nil
continue
}
// Collate all the matches into licenses and mutators.
for _, m := range b.Matches {
lic := allLicenses[m]
if lic.LicenseInfo.IsMutator {
// If the match is a mutator, add it to the mutators.
if !containsLicID(currentMutators, m) {
currentMutators = append(currentMutators, lic)
}
if !containsLicID(newMutators, m) {
newMutators = append(newMutators, lic)
}
} else {
// Otherwise, add it to the base licenses.
if !containsLicID(currentLicenses, m) {
currentLicenses = append(currentLicenses, lic)
}
if !containsLicID(newLicenses, m) {
newLicenses = append(newLicenses, lic)
}
}
}
// If the current block contains more than one base license...
if len(newLicenses) > 1 {
// Apply mutators to previous state.
// TODO: This invalid state should be refactored away
if len(previousLicenses) > 1 {
return fmt.Errorf("Invalid state. Should be only one previous license.")
}
if len(previousLicenses) > 0 {
appliedMutation = appliedMutation || applyMutatorsInAffectedBlocks(affectedBlocks, previousLicenses[0], previousMutators)
}
previousLicenses, previousMutators, affectedBlocks = nil, nil, nil
} else if mutatorsAreCompatible(currentLicenses, currentMutators) {
// Don't apply yet...
// Update the previous state and continue.
previousLicenses = currentLicenses
previousMutators = currentMutators
affectedBlocks = []Block{b}
} else {
// Otherwise, the current state is invalid and we need to apply mutators on the previous state.
// TODO: This invalid state should be refactored away
if len(previousLicenses) > 1 {
return fmt.Errorf("Invalid state. Should be only one previous license.")
}
if len(previousLicenses) > 0 {
appliedMutation = appliedMutation || applyMutatorsInAffectedBlocks(affectedBlocks, previousLicenses[0], previousMutators)
}
previousLicenses = newLicenses
previousMutators = newMutators
affectedBlocks = append(affectedBlocks, b)
}
}
// If there are still any blocks left unprocessed, and it is a valid state...
if len(affectedBlocks) > 0 && mutatorsAreCompatible(previousLicenses, previousMutators) {
// ... apply mutators to this final state.
// TODO: This invalid state should be refactored away
if len(previousLicenses) > 1 {
return fmt.Errorf("Invalid state. Should be only one previous license.")
}
if len(previousLicenses) > 0 {
appliedMutation = appliedMutation || applyMutatorsInAffectedBlocks(affectedBlocks, previousLicenses[0], previousMutators)
}
}
// If any mutations were applied, we need to recalculate the licenses_found and license_matches results.
if appliedMutation {
licenseResults.Matches = recalculateMatchesFromBlocks(*licenseResults)
}
return nil
}
// mutatorsAreCompatible checks for incompatibility.
// If any mutator is not compatible with the base license (or, if there is no
// base license, each other) then this function will return false.
// Otherwise, this function will return true.
func mutatorsAreCompatible(baseLicenses []licenses.License, mutators []licenses.License) bool {
numLicenses := len(baseLicenses)
if numLicenses > 1 {
return false
}
if len(mutators) == 0 {
return true
}
if numLicenses == 1 {
// If there is a base license...
replacementMutatorsCount := 0
l := baseLicenses[0]
for _, mutator := range mutators {
if !mutator.LicenseInfo.SPDXException {
if replacementMutatorsCount++; replacementMutatorsCount > 1 {
return false
}
}
if !slices.Contains(mutator.LicenseInfo.EligibleLicenses, l.GetID()) {
return false
}
}
} else if len(mutators) > 1 {
// Otherwise, if there are multiple mutators...
var mutualLicenses []string
replacementMutatorsCount := 0
for i, mutator := range mutators {
if i == 0 {
// Start with the first eligible licenses.
mutualLicenses = mutator.LicenseInfo.EligibleLicenses
} else {
// ... they must all be mutually compatible.
if len(mutator.LicenseInfo.EligibleLicenses) < 1 {
return false
}
if !mutator.LicenseInfo.SPDXException {
if replacementMutatorsCount++; replacementMutatorsCount > 1 {
return false
}
}
var filteredMutualLicenses []string
for _, id := range mutualLicenses {
if slices.Contains(mutator.LicenseInfo.EligibleLicenses, id) {
filteredMutualLicenses = append(filteredMutualLicenses, id)
}
}
mutualLicenses = filteredMutualLicenses
}
if len(mutualLicenses) < 1 {
return false
}
}
}
// Otherwise, there is no base license and fewer than two mutators, so no incompatibility is possible.
return true
}
func applyMutatorsInAffectedBlocks(affectedBlocks []Block, base licenses.License, mutators []licenses.License) bool {
if len(mutators) < 1 || len(affectedBlocks) < 1 {
return false
}
// Separate the exception mutators from the replacement mutator, if any.
var replacementMutator *licenses.License
var exceptionMutators []licenses.License
for _, mutator := range mutators {
if mutator.LicenseInfo.SPDXException {
exceptionMutators = append(exceptionMutators, mutator)
} else {
replacementMutator = &mutator // TODO: what if this is not the one and only??? Invalid state? Refactor?
}
}
// Initialize the new, mutated license.
mutatedLicense := licenses.License{
SPDXLicenseID: base.SPDXLicenseID,
LicenseInfo: licenses.LicenseInfo{
Name: base.LicenseInfo.Name,
SPDXStandard: base.LicenseInfo.SPDXStandard,
OSIApproved: base.LicenseInfo.OSIApproved,
},
}
// If there is a replacement mutator, apply it first.
if replacementMutator != nil {
mutatedLicense.SPDXLicenseID = replacementMutator.SPDXLicenseID
mutatedLicense.LicenseInfo.Name = replacementMutator.LicenseInfo.Name
mutatedLicense.LicenseInfo.SPDXStandard = replacementMutator.LicenseInfo.SPDXStandard
}
// If there are exception mutators, apply them.
for _, e := range exceptionMutators {
mutatedLicense.SPDXLicenseID = mutatedLicense.SPDXLicenseID + " WITH " + e.SPDXLicenseID
mutatedLicense.LicenseInfo.Name = mutatedLicense.LicenseInfo.Name + " with " + e.LicenseInfo.Name
mutatedLicense.LicenseInfo.SPDXStandard = e.LicenseInfo.SPDXStandard && mutatedLicense.LicenseInfo.SPDXStandard
}
// TODO: Save the mutated license, if necessary, to the license matches.
// Set the Matches field on all affected Blocks.
// TODO: legacy was setting one ID here (not append). Test/refactor.
for i := range affectedBlocks {
affectedBlocks[i].Matches = append(affectedBlocks[i].Matches, mutatedLicense.GetID())
}
return true
}
// TODO: See if this has ANY IMPACT ON ANYTHING
func recalculateMatchesFromBlocks(licenseResults IdentifierResults) map[string][]Match {
newMatches := make(map[string][]Match)
// Iterate over blocks to rebuild matches in license matches.
offset := 0
for _, block := range licenseResults.Blocks {
blockMatches := block.Matches
begins := offset
ends := offset + len(block.Text) - 1
offset = ends + 1
for _, licenseId := range blockMatches {
// If matches are null, copyright, keyword, or acceptable skip them.
switch licenseId {
case "", "COPYRIGHT", "KEYWORD", "ACCEPTABLE":
continue
default:
newMatches[licenseId] = append(newMatches[licenseId], Match{Begins: begins, Ends: ends})
}
}
}
return newMatches
}
func containsLicID(lics []licenses.License, id string) bool {
if id == "" {
return false
}
for i := range lics {
if id == lics[i].GetID() {
return true
}
}
return false
}