tartrazine/utils.go
Clark Boylan 16a5ff8e22 Fix IsVendor() regex generation
The regex generation introduced by
20726a1de3
had a subtle but important bug in it. In particular the ^ and ^|/
prefixes were only applied to the first regex in the OR'd groupings and
not every regex in the grouping.

We fix this by wrapping each of those OR'd groups in a new non capturing
group to ensure the prefix applies to every entry in the OR'd listing.

Additionally we add three new quick tests of IsVendor to guard against
this regression in the future.

This fixes https://github.com/go-enry/go-enry/issues/135
2022-08-25 16:22:09 -07:00

247 lines
6.5 KiB
Go

package enry
import (
"bytes"
"path/filepath"
"regexp"
"sort"
"strings"
"github.com/go-enry/go-enry/v2/data"
"github.com/go-enry/go-enry/v2/regex"
)
const binSniffLen = 8000
var configurationLanguages = map[string]struct{}{
"XML": {},
"JSON": {},
"TOML": {},
"YAML": {},
"INI": {},
"SQL": {},
}
// IsConfiguration tells if filename is in one of the configuration languages.
func IsConfiguration(path string) bool {
language, _ := GetLanguageByExtension(path)
_, is := configurationLanguages[language]
return is
}
// IsImage tells if a given file is an image (PNG, JPEG or GIF format).
func IsImage(path string) bool {
extension := filepath.Ext(path)
if extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".gif" {
return true
}
return false
}
// GetMIMEType returns a MIME type of a given file based on its languages.
func GetMIMEType(path string, language string) string {
if mime, ok := data.LanguagesMime[language]; ok {
return mime
}
if IsImage(path) {
return "image/" + filepath.Ext(path)[1:]
}
return "text/plain"
}
// IsDocumentation returns whether or not path is a documentation path.
func IsDocumentation(path string) bool {
return matchRegexSlice(data.DocumentationMatchers, path)
}
// IsDotFile returns whether or not path has dot as a prefix.
func IsDotFile(path string) bool {
base := filepath.Base(filepath.Clean(path))
return strings.HasPrefix(base, ".") && base != "."
}
var isVendorRegExp *regexp.Regexp
// IsVendor returns whether or not path is a vendor path.
func IsVendor(path string) bool {
return isVendorRegExp.MatchString(path)
}
// IsTest returns whether or not path is a test path.
func IsTest(path string) bool {
return matchRegexSlice(data.TestMatchers, path)
}
// IsBinary detects if data is a binary value based on:
// http://git.kernel.org/cgit/git/git.git/tree/xdiff-interface.c?id=HEAD#n198
func IsBinary(data []byte) bool {
if len(data) > binSniffLen {
data = data[:binSniffLen]
}
if bytes.IndexByte(data, byte(0)) == -1 {
return false
}
return true
}
// GetColor returns a HTML color code of a given language.
func GetColor(language string) string {
if color, ok := data.LanguagesColor[language]; ok {
return color
}
if color, ok := data.LanguagesColor[GetLanguageGroup(language)]; ok {
return color
}
return "#cccccc"
}
func matchRegexSlice(exprs []regex.EnryRegexp, str string) bool {
for _, expr := range exprs {
if expr.MatchString(str) {
return true
}
}
return false
}
// IsGenerated returns whether the file with the given path and content is a
// generated file.
func IsGenerated(path string, content []byte) bool {
ext := strings.ToLower(filepath.Ext(path))
if _, ok := data.GeneratedCodeExtensions[ext]; ok {
return true
}
for _, m := range data.GeneratedCodeNameMatchers {
if m(path) {
return true
}
}
path = strings.ToLower(path)
for _, m := range data.GeneratedCodeMatchers {
if m(path, ext, content) {
return true
}
}
return false
}
func init() {
// We now collate the individual regexps that make up the VendorMatchers to
// produce a single large regexp which is around twice as fast to test than
// simply iterating through all the regexps or naïvely collating the
// regexps.
//
// ---
//
// data.VendorMatchers here is a slice containing individual regexps that
// match a vendor file therefore if we want to test if a filename is a
// Vendor we need to test whether that filename matches one or more of
// those regexps.
//
// Now we could test each matcher in turn using a shortcircuiting test i.e.
//
// func IsVendor(filename string) bool {
// for _, matcher := range data.VendorMatchers {
// if matcher.Match(filename) {
// return true
// }
// }
// return false
// }
//
// Or concatentate all these regexps using groups i.e.
//
// `(regexp1)|(regexp2)|(regexp3)|...`
//
// However both of these are relatively slow and they don't take advantage
// of the inherent structure within our regexps...
//
// If we look at our regexps there are essentially three types of regexp:
//
// 1. Those that start with `^`
// 2. Those that start with `(^|/)`
// 3. Others
//
// If we collate our regexps into these groups that will significantly
// reduce the likelihood of backtracking within the regexp trie matcher.
//
// A further improvement is to use non-capturing groups as otherwise the
// regexp parser, whilst matching, will have to allocate slices for
// matching positions. (A future improvement here could be in the use of
// enforcing non-capturing groups within the sub-regexps too.)
//
// Finally if we sort the segments we can help the matcher build a more
// efficient matcher and trie.
// alias the VendorMatchers to simplify things
matchers := data.VendorMatchers
// Create three temporary string slices for our three groups above - prefixes removed
caretStrings := make([]string, 0, 10)
caretSegmentStrings := make([]string, 0, 10)
matcherStrings := make([]string, 0, len(matchers))
// Walk the matchers and check their string representation for each group prefix, remove it and add to the respective group slices
for _, matcher := range matchers {
str := matcher.String()
if str[0] == '^' {
caretStrings = append(caretStrings, str[1:])
} else if str[0:5] == "(^|/)" {
caretSegmentStrings = append(caretSegmentStrings, str[5:])
} else {
matcherStrings = append(matcherStrings, str)
}
}
// Sort the strings within each group - a potential further improvement could be in simplifying within these groups
sort.Strings(caretSegmentStrings)
sort.Strings(caretStrings)
sort.Strings(matcherStrings)
// Now build the collated regexp
sb := &strings.Builder{}
// Start with group 1 - those that started with `^`
sb.WriteString("(?:^(?:(?:")
sb.WriteString(caretStrings[0])
for _, matcher := range caretStrings[1:] {
sb.WriteString(")|(?:")
sb.WriteString(matcher)
}
sb.WriteString(")))")
sb.WriteString("|")
// Now add group 2 - those that started with `(^|/)`
sb.WriteString("(?:(?:^|/)(?:(?:")
sb.WriteString(caretSegmentStrings[0])
for _, matcher := range caretSegmentStrings[1:] {
sb.WriteString(")|(?:")
sb.WriteString(matcher)
}
sb.WriteString(")))")
sb.WriteString("|")
// Finally add the rest
sb.WriteString("(?:")
sb.WriteString(matcherStrings[0])
for _, matcher := range matcherStrings[1:] {
sb.WriteString(")|(?:")
sb.WriteString(matcher)
}
sb.WriteString(")")
// Compile the whole thing as the isVendorRegExp
isVendorRegExp = regexp.MustCompile(sb.String())
}