2017-06-13 13:56:07 +02:00
|
|
|
package enry
|
2016-07-13 19:05:09 +02:00
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"path/filepath"
|
2021-03-31 21:34:43 +02:00
|
|
|
"regexp"
|
|
|
|
"sort"
|
2016-07-13 19:05:09 +02:00
|
|
|
"strings"
|
2017-06-08 12:28:36 +02:00
|
|
|
|
2020-03-19 17:31:29 +01:00
|
|
|
"github.com/go-enry/go-enry/v2/data"
|
2020-04-15 17:27:48 +02:00
|
|
|
"github.com/go-enry/go-enry/v2/regex"
|
2016-07-13 19:05:09 +02:00
|
|
|
)
|
|
|
|
|
2019-02-05 22:54:14 +01:00
|
|
|
const binSniffLen = 8000
|
2017-05-29 10:05:16 +02:00
|
|
|
|
2020-05-27 15:07:57 +02:00
|
|
|
var configurationLanguages = map[string]struct{}{
|
|
|
|
"XML": {},
|
|
|
|
"JSON": {},
|
|
|
|
"TOML": {},
|
|
|
|
"YAML": {},
|
|
|
|
"INI": {},
|
|
|
|
"SQL": {},
|
2016-07-18 16:20:12 +02:00
|
|
|
}
|
|
|
|
|
2019-02-05 22:54:14 +01:00
|
|
|
// IsConfiguration tells if filename is in one of the configuration languages.
|
2016-07-13 22:21:18 +02:00
|
|
|
func IsConfiguration(path string) bool {
|
2017-06-12 13:42:20 +02:00
|
|
|
language, _ := GetLanguageByExtension(path)
|
|
|
|
_, is := configurationLanguages[language]
|
2016-07-13 22:21:18 +02:00
|
|
|
return is
|
|
|
|
}
|
|
|
|
|
2019-02-05 22:54:14 +01:00
|
|
|
// IsImage tells if a given file is an image (PNG, JPEG or GIF format).
|
2017-07-11 12:27:48 +02:00
|
|
|
func IsImage(path string) bool {
|
|
|
|
extension := filepath.Ext(path)
|
2017-07-11 11:13:49 +02:00
|
|
|
if extension == ".png" || extension == ".jpg" || extension == ".jpeg" || extension == ".gif" {
|
|
|
|
return true
|
|
|
|
}
|
2017-07-11 12:27:48 +02:00
|
|
|
|
2017-07-11 11:13:49 +02:00
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
2019-02-05 22:54:14 +01:00
|
|
|
// GetMIMEType returns a MIME type of a given file based on its languages.
|
|
|
|
func GetMIMEType(path string, language string) string {
|
2017-07-10 12:59:39 +02:00
|
|
|
if mime, ok := data.LanguagesMime[language]; ok {
|
|
|
|
return mime
|
|
|
|
}
|
|
|
|
|
2017-07-11 12:27:48 +02:00
|
|
|
if IsImage(path) {
|
|
|
|
return "image/" + filepath.Ext(path)[1:]
|
|
|
|
}
|
|
|
|
|
2017-07-10 12:59:39 +02:00
|
|
|
return "text/plain"
|
2017-07-10 12:50:52 +02:00
|
|
|
}
|
|
|
|
|
2019-02-05 22:54:14 +01:00
|
|
|
// IsDocumentation returns whether or not path is a documentation path.
|
|
|
|
func IsDocumentation(path string) bool {
|
2020-04-15 17:27:48 +02:00
|
|
|
return matchRegexSlice(data.DocumentationMatchers, path)
|
2019-02-05 22:54:14 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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 != "."
|
|
|
|
}
|
|
|
|
|
2021-03-31 21:34:43 +02:00
|
|
|
var isVendorRegExp *regexp.Regexp
|
|
|
|
|
2019-02-05 22:54:14 +01:00
|
|
|
// IsVendor returns whether or not path is a vendor path.
|
|
|
|
func IsVendor(path string) bool {
|
2021-03-31 21:34:43 +02:00
|
|
|
return isVendorRegExp.MatchString(path)
|
2019-02-05 22:54:14 +01:00
|
|
|
}
|
2016-07-13 22:21:18 +02:00
|
|
|
|
2020-04-06 16:23:48 +02:00
|
|
|
// IsTest returns whether or not path is a test path.
|
|
|
|
func IsTest(path string) bool {
|
2020-04-15 17:27:48 +02:00
|
|
|
return matchRegexSlice(data.TestMatchers, path)
|
2020-04-06 16:23:48 +02:00
|
|
|
}
|
|
|
|
|
2017-05-29 10:05:16 +02:00
|
|
|
// 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
|
2016-07-13 22:21:18 +02:00
|
|
|
func IsBinary(data []byte) bool {
|
2019-02-05 22:54:14 +01:00
|
|
|
if len(data) > binSniffLen {
|
|
|
|
data = data[:binSniffLen]
|
2016-07-13 22:21:18 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if bytes.IndexByte(data, byte(0)) == -1 {
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
return true
|
|
|
|
}
|
2019-07-19 22:28:57 +02:00
|
|
|
|
|
|
|
// GetColor returns a HTML color code of a given language.
|
|
|
|
func GetColor(language string) string {
|
|
|
|
if color, ok := data.LanguagesColor[language]; ok {
|
|
|
|
return color
|
|
|
|
}
|
|
|
|
|
2020-03-21 14:37:39 +01:00
|
|
|
if color, ok := data.LanguagesColor[GetLanguageGroup(language)]; ok {
|
|
|
|
return color
|
|
|
|
}
|
|
|
|
|
2019-07-19 22:28:57 +02:00
|
|
|
return "#cccccc"
|
|
|
|
}
|
2020-04-15 17:27:48 +02:00
|
|
|
|
|
|
|
func matchRegexSlice(exprs []regex.EnryRegexp, str string) bool {
|
|
|
|
for _, expr := range exprs {
|
|
|
|
if expr.MatchString(str) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return false
|
|
|
|
}
|
2020-05-27 15:07:57 +02:00
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
2021-03-31 21:34:43 +02:00
|
|
|
|
|
|
|
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())
|
|
|
|
}
|