internal/platform/embed.go (210 lines of code) (raw):
/*
* Copyright 2021-2024 JetBrains s.r.o.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package platform
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"fmt"
"io"
"log"
"os"
"path/filepath"
"runtime"
"strings"
"github.com/JetBrains/qodana-cli/internal/platform/product"
"github.com/JetBrains/qodana-cli/internal/platform/thirdpartyscan"
"github.com/JetBrains/qodana-cli/internal/platform/utils"
"github.com/JetBrains/qodana-cli/internal/tooling"
)
const (
converterJar = "intellij-report-converter.jar"
fuserJar = "qodana-fuser.jar"
baselineCli = "baseline-cli.jar"
)
// Mount a third-party linter.
func extractUtils(linter ThirdPartyLinter, cacheDir string) thirdpartyscan.MountInfo {
mountPath := getToolsMountPath(cacheDir)
javaPath, err := utils.GetJavaExecutablePath()
if err != nil {
log.Fatalf(
"Java is not installed or not accessible from PATH: %s. "+
"See requirements in our documentation: https://www.jetbrains.com/help/qodana/deploy-qodana.html",
err,
)
}
customTools, err := linter.MountTools(mountPath)
if err != nil {
log.Fatal(err)
}
converter := ProcessAuxiliaryTool(converterJar, "converter", mountPath, tooling.Converter)
fuser := ProcessAuxiliaryTool(fuserJar, "FUS", mountPath, tooling.Fuser)
baselineCliJar := ProcessAuxiliaryTool(
baselineCli,
"baseline-cli",
mountPath,
tooling.BaselineCli,
)
mountInfo := thirdpartyscan.MountInfo{
Converter: converter,
Fuser: fuser,
BaselineCli: baselineCliJar,
CustomTools: customTools,
JavaPath: javaPath,
}
return mountInfo
}
func getToolsMountPath(cacheDir string) string {
mountPath := filepath.Join(cacheDir, product.ShortVersion)
if _, err := os.Stat(mountPath); err != nil {
if os.IsNotExist(err) {
err = os.MkdirAll(mountPath, 0755)
if err != nil {
log.Fatal(err)
}
}
}
return mountPath
}
func ProcessAuxiliaryTool(toolName, moniker, mountPath string, bytes []byte) string {
toolPath := filepath.Join(mountPath, toolName)
if _, err := os.Stat(toolPath); err != nil {
if os.IsNotExist(err) {
err := os.WriteFile(toolPath, bytes, 0644)
if err != nil { // change the second parameter depending on which tool you have to process
log.Fatalf("Failed to write %s : %s", moniker, err)
}
}
}
return toolPath
}
func Decompress(archivePath string, destPath string) error {
isZip := strings.HasSuffix(archivePath, ".zip")
if //goland:noinspection GoBoolExpressions
isZip || runtime.GOOS == "windows" {
err, done := unpackZip(archivePath, destPath)
if done {
return err
}
} else {
err, done := extractTarGz(archivePath, destPath)
if done {
return err
}
}
return nil
}
// unpackZip unpacks zip archive to the destination path
func unpackZip(archivePath string, destPath string) (error, bool) {
zipReader, err := zip.OpenReader(archivePath)
if err != nil {
return err, true
}
defer func(zipReader *zip.ReadCloser) {
err = zipReader.Close()
if err != nil {
log.Fatal(err)
}
}(zipReader)
for _, f := range zipReader.File {
fpath := filepath.Join(destPath, f.Name)
// Check for Path Traversal
if !strings.HasPrefix(fpath, filepath.Clean(destPath)+string(os.PathSeparator)) {
return fmt.Errorf("%s: illegal file path", fpath), true
}
if f.FileInfo().IsDir() {
err = os.MkdirAll(fpath, os.ModePerm)
if err != nil {
return err, true
}
continue
}
if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
return err, true
}
dst, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
if err != nil {
return err, true
}
src, err := f.Open()
if err != nil {
return err, true
}
_, err = io.Copy(dst, src)
if err != nil {
return err, true
}
err = dst.Close()
if err != nil {
return err, true
}
err = src.Close()
if err != nil {
return err, true
}
}
return nil, false
}
// extractTarGz extracts tar.gz archive to the destination path
func extractTarGz(archivePath string, destPath string) (error, bool) {
archiveFile, err := os.Open(archivePath)
if err != nil {
return err, true
}
defer func(archiveFile *os.File) {
err := archiveFile.Close()
if err != nil {
log.Fatal(err)
}
}(archiveFile)
gzipReader, err := gzip.NewReader(archiveFile)
if err != nil {
return err, true
}
defer func(gzipReader *gzip.Reader) {
err := gzipReader.Close()
if err != nil {
log.Fatal(err)
}
}(gzipReader)
tarReader := tar.NewReader(gzipReader)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return err, true
}
target := filepath.Join(destPath, header.Name)
if !isInDirectory(destPath, target) {
return fmt.Errorf("%s: illegal file path", target), true
}
switch header.Typeflag {
case tar.TypeDir:
if _, err := os.Stat(target); err != nil {
if err = os.MkdirAll(target, 0755); err != nil {
return err, true
}
}
case tar.TypeReg:
file, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
if err != nil {
return err, true
}
if _, err := io.Copy(file, tarReader); err != nil {
err := file.Close()
if err != nil {
return err, true
}
return err, true
}
err = file.Close()
if err != nil {
return err, true
}
}
}
return nil, false
}
// isInDirectory checks if the target file is within the destination directory.
func isInDirectory(destPath string, target string) bool {
relative, err := filepath.Rel(destPath, target)
if err != nil {
return false
}
return !strings.HasPrefix(relative, "..")
}