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 old is an old (v0.1.0) copy of the licensecheck package, for easier comparison with the new Scan API.
package old

import (
	
	
	
	
)
Options allow us to adjust parameters for the matching algorithm. TODO: Delete this once the package has been fine-tuned.
type Options struct {
	MinLength int // Minimum length of run, in words, to count as a matching substring.
	Threshold int // Percentage threshold to report a match.
	Slop      int // Maximum allowable gap in a near-contiguous match.
}

var defaults = Options{
	MinLength: 10,
	Threshold: 40,
	Slop:      8,
}
Type groups the licenses into various classifications. TODO: This list is clearly incomplete.
type Type int

const (
	AGPL Type = iota
	Apache
	BSD
	CC
	GPL
	JSON
	MIT
	Unlicense
	Zlib
	Other
	NumTypes = Other
)

func ( string) Type {
	for  := Type(0);  < NumTypes; ++ {
		if strings.HasPrefix(, .String()) {
			return 
		}
	}
	return Other
}
A phrase is a sequence of words used as a key for startIndexes. Empirically, two words are best; more is slower.
type phrase [2]int32

type license struct {
	typ  Type
	name string
	text string
	doc  *document
}

type document struct {
	text    []byte  // Original text.
	words   []int32 // Normalized words (indexes into c.words)
	byteOff []int32 // ith byteOff is byte offset of ith word in original text.
}
A Checker matches a set of known licenses.
type Checker struct {
	licenses []license
	urls     map[string]string
	dict     map[string]int32 // dict maps word to index in words
	words    []string         // list of known words
	index    map[phrase][]indexEntry
}

type indexEntry struct {
	licenseID int32
	start     int32
}
A License describes a single license that can be recognized. At least one of the Text or the URL should be set.
New returns a new Checker that recognizes the given list of licenses.
func ( []License) *Checker {
	 := &Checker{
		licenses: make([]license, 0, len()),
		urls:     make(map[string]string),
		dict:     make(map[string]int32),
		index:    make(map[phrase][]indexEntry),
	}
	for ,  := range  {
		if .Text != "" {
			 := len(.licenses)
			.licenses = .licenses[:+1]
			 := &.licenses[]
			.name = .Name
			.typ = licenseType(.name)
			.text = .Text
			.doc = .normalize([]byte(.text), true)
			.updateIndex(int32(), .doc.words)
		}
		if .URL != "" {
			.urls[.URL] = .Name
		}
	}

	return 
}
Initialized in func init in data.gen.go.
BuiltinLicenses returns the list of licenses built into the package. That is, the built-in checker is equivalent to New(BuiltinLicenses()).
Return a copy so caller cannot change list entries.
	return append([]License{}, builtinList...)
}
Coverage describes how the text matches various licenses.
Percent is the fraction of the total text, in normalized words, that matches any valid license, expressed as a percentage across all of the licenses matched.
Match describes, in sequential order, the matches of the input text across the various licenses. Typically it will be only one match long, but if the input text is a concatenation of licenses it will contain a match value for each element of the concatenation.
	Match []Match
}
When we build the Match, Start and End are word offsets, but they are converted to byte offsets in the original before being passed back to the caller.
Match describes how a section of the input matches a license.
type Match struct {
	Name    string  // The (file) name of the license it matches.
	Type    Type    // The type of the license: BSD, MIT, etc.
	Percent float64 // The fraction of words between Start and End that are matched.
	Start   int     // The byte offset of the first word in the input that matches.
IsURL reports that the matched text identifies a license by indirection through a URL. If set, Start and End specify the location of the URL itself, and Percent is always 100.0.
	IsURL bool
}

type submatch struct {
	licenseID  int32 // Index of license in c.licenses
	start      int   // Index of starting word.
	end        int   // Index of first following word.
Number of words between start and end that actually match. Because of slop, this can be less than end-start.
updateIndex is used during initialization to construct a map from the occurrences of each phrase in any license to the word offset in that license, like an n-gram posting list index in full-text search.
func ( *Checker) ( int32,  []int32) {
	var  phrase
	const  = len()
	for  := 0; + <= len(); ++ {
		copy([:], [:])
		.index[] = append(.index[], indexEntry{, int32()})
	}
}
Cover computes the coverage of the text according to the license set compiled into the package. An input text may match multiple licenses. If that happens, Match contains only disjoint matches. If multiple licenses match a particular section of the input, the best match is chosen so the returned coverage describes at most one match for each section of the input.
func ( []byte,  Options) (Coverage, bool) {
	return builtin.Cover(, )
}
Cover is like the top-level function Cover, but it uses the set of licenses in the Checker instead of the built-in license set.
func ( *Checker) ( []byte,  Options) (Coverage, bool) {
	 := .normalize(, false)
Match the input text against all licenses.
	var  []Match
	for ,  := range .submatches(.words, ) {
		 = append(, makeMatch(&.licenses[.licenseID], ))
	}
	.sort()
We have potentially multiple candidate matches and must winnow them down to the best non-overlapping set. Do this by noticing when two overlap, and killing off the one that matches fewer words in the text, including the slop.
	 := make([]bool, len())
	 := float64(.Threshold)
	if  <= 0 {
		 = float64(defaults.Threshold)
	}
	for  := range  {
		if [].Percent <  {
			[] = true
		}
	}
	for  := range  {
		if [] {
			continue
		}
		 := &[]
		 := .Percent * float64(.End-.Start)
		for  := range  {
			if [] ||  ==  {
				continue
			}
			 := &[]
			if .overlaps() {
				 := 
				if  > .Percent*float64(.End-.Start) {
					 = 
				}
				[] = true
			}
		}
	}
	 := [:0]
	for  := range  {
		if ![] {
			 = append(, [])
		}
	}
	 = 
Look for URLs in the gaps.
Sort again.
		 = append(, ...)
		.sort()
	}
Compute this before overwriting offsets.
	 := .percent()

	.toByteOffsets(, )

	return Coverage{
		Percent: ,
		Match:   ,
	}, len() > 0
}

func ( *document) ( []Match) {
	sort.Slice(, func(,  int) bool {
		 := &[]
		 := &[]
		if .Start != .Start {
			return .Start < .Start
		}
		return .Name < .Name
	})
}

func ( *document) ( int) int {
	for ,  := range .byteOff {
		if int() >=  {
			return 
		}
	}
	return len(.words)
}
endWordToEndByte returns the end byte offset corresponding to the given end word offset.
func ( *document) ( *Checker,  int) int {
	if  == 0 {
		return 0
	}
	if  == len(.words) {
		return len(.text)
	}
	if .words[-1] >= 0 {
		return int(.byteOff[-1]) + len(.words[.words[-1]])
	}
Unknown word in document, not added to dictionary. Look in text to find out how long it is.
	 := int(.byteOff[-1])
	for  < len(.text) {
		,  := utf8.DecodeRune(.text[:])
		if !isWordChar() {
			break
		}
		 += 
	}
	return 
}
toByteOffsets converts in-place the non-URL Matches' word offsets in the document to byte offsets.
func ( *document) ( *Checker,  []Match) {
	for  := range  {
		 := &[]
		 := .Start
		if  == 0 {
			.Start = 0
		} else {
			.Start = int(.byteOff[])
		}
		.End = .endWordToEndByte(, .End)
	}
}
The regular expression is a simplified finder of URLS. We assume licenses are going to have fairly simple URLs, and in practice they do. See urls.go. Matching is case-insensitive.
const (
	pathRE   = `[-a-z0-9_.#?=]+` // Paths plus queries.
	domainRE = `[-a-z0-9_.]+`
)

var urlRE = regexp.MustCompile(`(?i)https?://(` + domainRE + `)+(\.org|com)(/` + pathRE + `)+/?`)
findURLsBetween returns a slice of Matches holding URLs of licenses, to be inserted into the total list of Matches.
func ( *document) ( *Checker,  []Match) []Match {
	var  []Match
	 := 0
	for  := 0;  <= len(); ++ {
		 := 
		 := len(.words)
		if  < len() {
			 = [].Start
			 = [].End
		}
If there's not enough words here for a URL, like http://b.co, then don't try.
		if  < +3 {
			continue
		}
Since doc.words excludes numerals, the last "word" might not actually be the last text in the file. Make sure to run to EOF if we're at the end. Otherwise, the end will go right up to the start of the next match, and that will include all the text in the gap.
		 := .endWordToEndByte(, )
		 := urlRE.FindAllIndex(.text[:], -1)
		if len() == 0 {
			continue
		}
		for ,  := range  {
			,  := [0]+, [1]+
			if ,  := .licenseURL(string(.text[:]));  {
				 = append(, Match{
					Name:    ,
					Type:    licenseType(),
					Percent: 100.0, // 100% of Start:End is a license URL.
					Start:   .wordOffset(),
					End:     .wordOffset(),
					IsURL:   true,
				})
			}
		}
	}
	return 
}
licenseURL reports whether url is a known URL, and returns its name if it is.
We need to canonicalize the text for lookup. First, trim the leading http:// or https:// and the trailing /. Then we lower-case it.
	 = strings.TrimPrefix(, "http://")
	 = strings.TrimPrefix(, "https://")
	 = strings.TrimSuffix(, "/")
	 = strings.TrimSuffix(, "/legalcode") // Common for CC licenses.
	 = strings.ToLower()
	,  := .urls[]
	if  {
		return , true
	}
Try trimming one more path element, so that the ported URL https://creativecommons.org/licenses/by/3.0/us/ is recognized as the known unported URL https://creativecommons.org/licenses/by/3.0
	if  := strings.LastIndex(, "/");  >= 0 {
		if ,  = .urls[[:]];  {
			return , true
		}
	}

	return "", false
}
percent returns the total percentage of words in the input matched by matches. When it is called, matches (except for URLs) are in units of words.
func ( *document) ( []Match) float64 {
	if len(.words) == 0 {
		return 0 // avoid NaN
	}
	 := 0
	for ,  := range  {
		if .IsURL {
			 += .endPos(, ) - .startPos(, )
		} else {
			 += .End - .Start
			continue
		}
	}
	return 100 * float64() / float64(len(.words))
}
startPos returns the starting position of match i for purposes of computing coverage percentage. For URLs, it's tricky because Start and End refer to the URL itself, so we presume the match covers the whole gap.
func ( *document) ( []Match,  int) int {
	 := []
	if !.IsURL {
		return .Start
This is a URL match.
	if  == 0 {
		return 0
Is the previous match a URL? If so, split the gap. If not, take the whole gap.
	 := [-1]
	if !.IsURL {
		return .End
	}
	return (.Start + .End) / 2
}
endPos is the complement of startPos.
func ( *document) ( []Match,  int) int {
	 := []
	if !.IsURL {
		return .End
	}
	if  == len()-1 {
		return len(.words)
	}
	 := [+1]
	if !.IsURL {
		return .Start
	}
	return (.End + .Start) / 2
}

func ( *license,  submatch) Match {
	var  Match
	.Name = .name
	.Type = .typ
	.Percent = 100 * float64(.matched) / float64(len(.doc.words))
	.Start = .start
	.End = .Start + (.end - .start)
	return 
}
overlaps reports whether the two matches represent at least part of the same text.
func ( *Match) ( *Match) bool {
	return .Start < .End && .Start < .End
}
submatches returns a list describing the runs of words in text that match any of the licenses. Its algorithm is a heuristic and can be defeated, but seems to work well in practice.
func ( *Checker) ( []int32,  Options) []submatch {
	if len() == 0 {
		return nil
	}
	if .MinLength <= 0 {
		.MinLength = defaults.MinLength
	}
	if .Slop <= 0 {
		.Slop = defaults.Slop
	}

	var  []submatch
byLicense maps a license ID to the index of the last entry in matches recording a match of that license. Sometimes we extend the last match instead of adding a new one.
	 := make([]int, len(.licenses))
	for  := range  {
		[] = -1
	}
For each word of the input, look to see if a sequence starting there matches a sequence in any of the licenses.
	var  phrase
Look up current phrase in the index (posting list) to find possible match locations.
		copy([:], [:])
		 := .index[]
		for len() > 0 {
			 := [0].licenseID
If this start index is for a license that we've already matched beyond k, skip over all the start indexes for that license.
			if  := [];  >= 0 &&  < [].end {
				for len() > 0 && [0].licenseID ==  {
					 = [1:]
				}
				continue
			}
Find longest match within the possible starts in this license.
			 := 0 // start in l.doc
			 := 0
			 := &.licenses[]
			for len() > 0 && [0].licenseID ==  {
				 := [0]
				 = [1:]
				 :=  + len()
				for ,  := range .doc.words[int(.start)+len():] {
					if  == len() ||  != [] {
						break
					}
					++
				}
				if - >  {
					 =  - 
					 = int(.start)
				}
			}

			if  < .MinLength {
				continue
			}
We have a long match - the longest for this license. Remember it. Note that we do not do anything to advance the license text, which means that certain reorderings will match, perhaps erroneously. This has not appeared in practice, while handling things this way means the algorithm can identify multiple appearances of a license within a single file.
			 := 
			 :=  + 
The blank (wildcard) ___ maps to word ID -1. If we see a blank, we allow it to be filled in by up to 70 words. This allows recognizing quite a few specialized copyright lines (see for example testdata/MIT.t2) while not being large enough to jump over an entire other license (our shortest is Apache-2.0-User at 80 words).
			const  = 70
Does this fit onto the previous match, or is it close enough to consider? The slop allows text like Copyright (c) 2009 Snarfboodle Inc. All rights reserved. to match Copyright (c) <YEAR> <COMPANY>. All rights reserved. and be considered a single span.
			if  := [];  >= 0 {
				 := &[]
				 := .Slop
				if .licenseEnd < len(.doc.words) && .doc.words[.licenseEnd] == blankID {
					 = 
				}
				if .end+ >=  &&  >= .licenseEnd {
					if  ==  {
						.matched++ // matched the blank
					}
					.end = 
					.matched += 
					.licenseEnd =  + 
					continue
				}
			}
Does this match immediately follow an early blank in the license text? If so, see if we can extend it backward. The most common case needing this is licenses that start with "Copyright ___". The text before the blank is too short to be its own match but it can be part of this one. This is a for loop instead of an if statement to allow backing up over multiple nearby blanks, such as in licenses/ISC.
		:
			for  >= 2 && .doc.words[-1] == blankID && .doc.words[-2] != blankID {
				 :=  - 
				if  < 0 {
					 = 0
				}
				if  := [];  >= 0 &&  < [].end {
					 = [].end
				}
				for  :=  - 1;  >= ; -- {
Found a match across the gap.
						 = 
						 -= 2
Extend backward if possible.
						for  > 0 &&  > 0 && [-1] == .doc.words[-1] {
							--
							--
							++
See if we're up against another blank.
						continue 
					}
				}
				break
			}

			[] = len()
			 = append(, submatch{
				start:      ,
				end:        ,
				matched:    ,
				licenseEnd:  + ,
				licenseID:  ,
			})
		}
	}
	return