mirror of
https://github.com/github/codeql.git
synced 2025-12-16 16:53:25 +01:00
315 lines
8.5 KiB
Go
315 lines
8.5 KiB
Go
package util
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"log"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
var extractorPath string
|
|
|
|
// Getenv retrieves the value of the environment variable named by the key.
|
|
// If that variable is not present, it iterates over the given aliases until
|
|
// it finds one that is. If none are present, the empty string is returned.
|
|
func Getenv(key string, aliases ...string) string {
|
|
val := os.Getenv(key)
|
|
if val != "" {
|
|
return val
|
|
}
|
|
for _, alias := range aliases {
|
|
val = os.Getenv(alias)
|
|
if val != "" {
|
|
return val
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// FileExists tests whether the file at `filename` exists and is not a directory.
|
|
func FileExists(filename string) bool {
|
|
info, err := os.Stat(filename)
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
log.Printf("Unable to stat %s: %s\n", filename, err.Error())
|
|
}
|
|
return err == nil && !info.IsDir()
|
|
}
|
|
|
|
// DirExists tests whether `filename` exists and is a directory.
|
|
func DirExists(filename string) bool {
|
|
info, err := os.Stat(filename)
|
|
if err != nil && !errors.Is(err, fs.ErrNotExist) {
|
|
log.Printf("Unable to stat %s: %s\n", filename, err.Error())
|
|
}
|
|
return err == nil && info.IsDir()
|
|
}
|
|
|
|
func RunCmd(cmd *exec.Cmd) bool {
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
in, _ := cmd.StdinPipe()
|
|
err := cmd.Start()
|
|
if err != nil {
|
|
log.Printf("Running %s %v failed, continuing anyway: %s\n", cmd.Path, cmd.Args, err.Error())
|
|
return false
|
|
}
|
|
in.Close()
|
|
err = cmd.Wait()
|
|
if err != nil {
|
|
log.Printf("Running %s %v failed, continuing anyway: %s\n", cmd.Path, cmd.Args, err.Error())
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
func getOsToolsSubdir() (string, error) {
|
|
platform, set := os.LookupEnv("CODEQL_PLATFORM")
|
|
if set && platform != "" {
|
|
return platform, nil
|
|
}
|
|
|
|
switch runtime.GOOS {
|
|
case "darwin":
|
|
return "osx64", nil
|
|
case "linux":
|
|
return "linux64", nil
|
|
case "windows":
|
|
return "win64", nil
|
|
}
|
|
return "", errors.New("Unsupported OS: " + runtime.GOOS)
|
|
}
|
|
|
|
func getExtractorDir() (string, error) {
|
|
extractorRoot := os.Getenv("CODEQL_EXTRACTOR_GO_ROOT")
|
|
if extractorRoot == "" {
|
|
log.Print("CODEQL_EXTRACTOR_GO_ROOT not set.\nThis binary should not be run manually; instead, use the CodeQL CLI or VSCode extension. See https://securitylab.github.com/tools/codeql.\n")
|
|
log.Print("Falling back to guess the root based on this executable's path.\n")
|
|
|
|
mypath, err := os.Executable()
|
|
if err == nil {
|
|
return filepath.Dir(mypath), nil
|
|
} else {
|
|
return "", errors.New("CODEQL_EXTRACTOR_GO_ROOT not set, and could not determine path of this executable: " + err.Error())
|
|
}
|
|
}
|
|
|
|
osSubdir, err := getOsToolsSubdir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return filepath.Join(extractorRoot, "tools", osSubdir), nil
|
|
}
|
|
|
|
func GetExtractorPath() (string, error) {
|
|
if extractorPath != "" {
|
|
return extractorPath, nil
|
|
}
|
|
|
|
dirname, err := getExtractorDir()
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
extractorPath := filepath.Join(dirname, "go-extractor")
|
|
if runtime.GOOS == "windows" {
|
|
extractorPath = extractorPath + ".exe"
|
|
}
|
|
return extractorPath, nil
|
|
}
|
|
|
|
func EscapeTrapSpecialChars(s string) string {
|
|
// Replace TRAP special characters with their HTML entities, as well as '&' to avoid ambiguity.
|
|
s = strings.ReplaceAll(s, "&", "&")
|
|
s = strings.ReplaceAll(s, "{", "{")
|
|
s = strings.ReplaceAll(s, "}", "}")
|
|
s = strings.ReplaceAll(s, "\"", """)
|
|
s = strings.ReplaceAll(s, "@", "@")
|
|
s = strings.ReplaceAll(s, "#", "#")
|
|
return s
|
|
}
|
|
|
|
func FindGoFiles(root string) bool {
|
|
found := false
|
|
filepath.WalkDir(root, func(s string, d fs.DirEntry, e error) error {
|
|
if e != nil {
|
|
return e
|
|
}
|
|
if filepath.Ext(d.Name()) == ".go" {
|
|
found = true
|
|
return filepath.SkipAll
|
|
}
|
|
return nil
|
|
})
|
|
return found
|
|
}
|
|
|
|
// The type of check function used by `FindAllFilesWithName` to decide whether to skip the directory named by `path`.
|
|
type FindAllFilesWithNameSkipCheck func(path string) bool
|
|
|
|
// Commonly we only want to skip `vendor` directories in `FindAllFilesWithName`. This array is a suitable
|
|
// argument for `dirsToSkip` which skips `vendor` directories.
|
|
var SkipVendorChecks = []FindAllFilesWithNameSkipCheck{IsGolangVendorDirectory}
|
|
|
|
// Returns an array of all files matching `name` within the path at `root`.
|
|
// The `dirsToSkip` array contains check functions used to decide which directories to skip.
|
|
func FindAllFilesWithName(root string, name string, dirsToSkip ...FindAllFilesWithNameSkipCheck) []string {
|
|
paths := make([]string, 0, 1)
|
|
filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() {
|
|
for _, dirToSkip := range dirsToSkip {
|
|
if dirToSkip(path) {
|
|
return filepath.SkipDir
|
|
}
|
|
}
|
|
}
|
|
if d.Name() == name {
|
|
paths = append(paths, path)
|
|
}
|
|
return nil
|
|
})
|
|
return paths
|
|
}
|
|
|
|
// Returns an array of any Go source files in locations which do not have a `go.mod`
|
|
// file in the same directory or higher up in the file hierarchy, relative to the `root`.
|
|
func GoFilesOutsideDirs(root string, dirsToSkip ...string) []string {
|
|
result := []string{}
|
|
|
|
filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if d.IsDir() && slices.Contains(dirsToSkip, path) {
|
|
return filepath.SkipDir
|
|
}
|
|
if filepath.Ext(d.Name()) == ".go" {
|
|
result = append(result, path)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if len(result) > 0 {
|
|
log.Printf(
|
|
"Found %d stray Go source file(s) in %s\n",
|
|
len(result),
|
|
JoinTruncatedList(result, ", ", 5),
|
|
)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// Joins the `elements` into one string, up to `maxElements`, separated by `sep`.
|
|
// If the length of `elements` exceeds `maxElements`, the string "and %d more" is
|
|
// appended where `%d` is the number of `elements` that were omitted.
|
|
func JoinTruncatedList(elements []string, sep string, maxElements int) string {
|
|
num := len(elements)
|
|
numIncluded := num
|
|
truncated := false
|
|
if num > maxElements {
|
|
numIncluded = maxElements
|
|
truncated = true
|
|
}
|
|
|
|
result := strings.Join(elements[0:numIncluded], sep)
|
|
if truncated {
|
|
result += fmt.Sprintf(", and %d more", num-maxElements)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// For every file path in the input array, return the parent directory.
|
|
func GetParentDirs(paths []string) []string {
|
|
dirs := make([]string, len(paths))
|
|
for i, path := range paths {
|
|
dirs[i] = filepath.Dir(path)
|
|
}
|
|
return dirs
|
|
}
|
|
|
|
// Returns the import path of the package being built, or "" if it cannot be determined.
|
|
func GetImportPath() (importpath string) {
|
|
importpath = os.Getenv("LGTM_INDEX_IMPORT_PATH")
|
|
if importpath == "" {
|
|
repourl := os.Getenv("SEMMLE_REPO_URL")
|
|
if repourl == "" {
|
|
githubrepo := os.Getenv("GITHUB_REPOSITORY")
|
|
if githubrepo == "" {
|
|
log.Printf("Unable to determine import path, as neither LGTM_INDEX_IMPORT_PATH nor GITHUB_REPOSITORY is set\n")
|
|
return ""
|
|
} else {
|
|
importpath = "github.com/" + githubrepo
|
|
}
|
|
} else {
|
|
importpath = getImportPathFromRepoURL(repourl)
|
|
if importpath == "" {
|
|
log.Printf("Failed to determine import path from SEMMLE_REPO_URL '%s'\n", repourl)
|
|
return
|
|
}
|
|
}
|
|
}
|
|
log.Printf("Import path is '%s'\n", importpath)
|
|
return
|
|
}
|
|
|
|
// Returns the import path of the package being built from `repourl`, or "" if it cannot be
|
|
// determined.
|
|
func getImportPathFromRepoURL(repourl string) string {
|
|
// check for scp-like URL as in "git@github.com:github/codeql-go.git"
|
|
shorturl := regexp.MustCompile(`^([^@]+@)?([^:]+):([^/].*?)(\.git)?$`)
|
|
m := shorturl.FindStringSubmatch(repourl)
|
|
if m != nil {
|
|
return m[2] + "/" + m[3]
|
|
}
|
|
|
|
// otherwise parse as proper URL
|
|
u, err := url.Parse(repourl)
|
|
if err != nil {
|
|
log.Fatalf("Malformed repository URL '%s'\n", repourl)
|
|
}
|
|
|
|
if u.Scheme == "file" {
|
|
// we can't determine import paths from file paths
|
|
return ""
|
|
}
|
|
|
|
if u.Hostname() == "" || u.Path == "" {
|
|
return ""
|
|
}
|
|
|
|
host := u.Hostname()
|
|
path := u.Path
|
|
// strip off leading slashes and trailing `.git` if present
|
|
path = regexp.MustCompile(`^/+|\.git$`).ReplaceAllString(path, "")
|
|
return host + "/" + path
|
|
}
|
|
|
|
// Decides if `path` refers to a file that exists.
|
|
func fileExists(path string) bool {
|
|
stat, err := os.Stat(path)
|
|
return err == nil && stat.Mode().IsRegular()
|
|
}
|
|
|
|
// Decides if `dirPath` is a vendor directory by testing whether it is called `vendor`
|
|
// and contains a `modules.txt` file.
|
|
func IsGolangVendorDirectory(dirPath string) bool {
|
|
return filepath.Base(dirPath) == "vendor" &&
|
|
(fileExists(filepath.Join(dirPath, "modules.txt")) ||
|
|
fileExists(filepath.Join(dirPath, "../glide.yaml")) ||
|
|
fileExists(filepath.Join(dirPath, "../Gopkg.lock")) ||
|
|
fileExists(filepath.Join(dirPath, "../vendor.conf")))
|
|
}
|