github.com/vchain-us/vcn@v0.9.11-0.20210921212052-a2484d23c0b3/pkg/bom/java/java.go (about)

     1  /*
     2   * Copyright (c) 2018-2020 vChain, Inc. All Rights Reserved.
     3   * This software is released under GPL3.
     4   * The full license information can be found under:
     5   * https://www.gnu.org/licenses/gpl-3.0.en.html
     6   *
     7   */
     8  
     9  package java
    10  
    11  import (
    12  	"archive/zip"
    13  	"bufio"
    14  	"bytes"
    15  	"crypto/sha256"
    16  	"encoding/base64"
    17  	"encoding/hex"
    18  	"encoding/xml"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"io/ioutil"
    23  	"os"
    24  	"os/exec"
    25  	"path/filepath"
    26  	"strings"
    27  
    28  	"golang.org/x/net/html/charset"
    29  
    30  	"github.com/schollz/progressbar/v3"
    31  	"github.com/sirupsen/logrus"
    32  
    33  	"github.com/vchain-us/vcn/pkg/bom/artifact"
    34  )
    35  
    36  const AssetType = "java"
    37  
    38  const mvn_version = 3
    39  const mvn_artid = 0
    40  const mvn_pkg_name = 1
    41  
    42  const pom = "pom.xml"
    43  const maven = "mvn"
    44  
    45  type task struct {
    46  	path    string
    47  	name    string
    48  	version string
    49  }
    50  type result struct {
    51  	comp artifact.Dependency
    52  	err  error
    53  }
    54  
    55  // javaMavenArtifact implements Artifact interface
    56  type javaMavenArtifact struct {
    57  	artifact.GenericArtifact
    58  	folder  string
    59  	dirName string
    60  }
    61  
    62  func (p *javaMavenArtifact) Path() string {
    63  	return p.dirName
    64  }
    65  
    66  // New returns new JavaMavenPackage object
    67  func New(path string) artifact.Artifact {
    68  	f, err := GetPOM(path)
    69  	if err != nil {
    70  		return nil
    71  	}
    72  
    73  	return &javaMavenArtifact{folder: f, dirName: filepath.Dir(path)}
    74  }
    75  
    76  func (p *javaMavenArtifact) Type() string {
    77  	return AssetType
    78  }
    79  
    80  // Dependencies returns list of java packages used during the build
    81  func (a *javaMavenArtifact) ResolveDependencies(output artifact.OutputOptions) ([]artifact.Dependency, error) {
    82  	if a.Deps != nil {
    83  		return a.Deps, nil
    84  	}
    85  
    86  	xmlDepFn, err := ioutil.TempFile(os.TempDir(), "xml")
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  	defer xmlDepFn.Close()
    91  
    92  	fname, err := exec.LookPath("mvn")
    93  	if err != nil {
    94  		return nil, fmt.Errorf("please install mvn tool follwing this link: https://maven.apache.org/install.html. Error reported: %w", err)
    95  	}
    96  
    97  	command := exec.Command(fname, "dependency:tree", "-DoutputType=graphml", "-f="+a.folder, "-DappendOutput=true", "-DoutputFile="+xmlDepFn.Name())
    98  	_, err = command.Output()
    99  	if err != nil {
   100  		exit, ok := err.(*exec.ExitError)
   101  		if ok {
   102  			return nil, fmt.Errorf("cannot get Java dependencies:\n%s", string(exit.Stderr))
   103  		}
   104  		return nil, fmt.Errorf("cannot get Java dependencies: %w", err)
   105  	}
   106  
   107  	xmlDep, err := ioutil.ReadFile(xmlDepFn.Name())
   108  	if err != nil {
   109  		return nil, err
   110  	}
   111  	graph := GraphML{}
   112  	err = xml.Unmarshal(xmlDep, &graph)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	cacheDir, err := MavenCacheDir()
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  
   122  	// worker pool
   123  	tasks := make(chan task, len(graph.Graph.Nodes))
   124  	results := make(chan result, len(graph.Graph.Nodes))
   125  	for i := 0; i < artifact.MaxGoroutines; i++ {
   126  		go worker(cacheDir, tasks, results)
   127  	}
   128  
   129  	for _, n := range graph.Graph.Nodes {
   130  		fields := strings.Split(n.Data.ShapeNode.NodeLabel, ":")
   131  		if fields[mvn_pkg_name] == "" {
   132  			logrus.Errorf("unable to retrieve package name of component %s", n.Data.ShapeNode.NodeLabel)
   133  			continue
   134  		}
   135  		if fields[mvn_version] == "" {
   136  			logrus.Errorf("unable to retrieve package version of component %s", n.Data.ShapeNode.NodeLabel)
   137  			continue
   138  		}
   139  		if fields[mvn_artid] == "" {
   140  			logrus.Errorf("unable to retrieve package artifact ID of component %s", n.Data.ShapeNode.NodeLabel)
   141  			continue
   142  		}
   143  
   144  		tasks <- task{
   145  			path:    strings.Replace(fields[mvn_artid], ".", "/", -1),
   146  			name:    fields[mvn_pkg_name],
   147  			version: fields[mvn_version],
   148  		}
   149  	}
   150  
   151  	res := make([]artifact.Dependency, 0)
   152  	bar := progressbar.Default(int64(len(graph.Graph.Nodes)))
   153  	for done := 0; done < len(graph.Graph.Nodes); done++ {
   154  		result := <-results
   155  		if result.err != nil {
   156  			// it happens sometimes when dependency specified, but it doesn't exist
   157  			continue
   158  		}
   159  		if result.comp.Hash != "" {
   160  			res = append(res, result.comp)
   161  		}
   162  		switch output {
   163  		case artifact.Progress:
   164  			bar.Add(1)
   165  		case artifact.Debug:
   166  			fmt.Printf("%s@%s (%s)\n", result.comp.Name, result.comp.Version, result.comp.Hash)
   167  		}
   168  	}
   169  	close(tasks)
   170  	close(results)
   171  
   172  	a.Deps = res
   173  	return res, nil
   174  }
   175  
   176  type MavenPOM struct {
   177  	License string `xml:"licenses>license>name"`
   178  }
   179  
   180  func worker(cacheDir string, tasks <-chan task, results chan<- result) {
   181  	for task := range tasks {
   182  		comp := artifact.Dependency{}
   183  
   184  		basePath := filepath.Join(cacheDir, task.path, task.name, task.version, task.name+"-"+task.version)
   185  
   186  		buf, err := ioutil.ReadFile(basePath + ".jar")
   187  		if err != nil {
   188  			results <- result{comp: comp, err: err}
   189  			continue
   190  		}
   191  		rawHash := sha256.Sum256(buf)
   192  		hash := hex.EncodeToString(rawHash[:])
   193  
   194  		buf, err = ioutil.ReadFile(basePath + ".pom")
   195  		if err != nil {
   196  			results <- result{comp: comp, err: err}
   197  			continue
   198  		}
   199  
   200  		var pom MavenPOM
   201  		decoder := xml.NewDecoder(bytes.NewBuffer(buf))
   202  		decoder.CharsetReader = charset.NewReaderLabel
   203  		err = decoder.Decode(&pom)
   204  		if err != nil {
   205  			results <- result{comp: comp, err: err}
   206  			continue
   207  		}
   208  
   209  		comp.Hash = hash
   210  		comp.Version = task.version
   211  		comp.Name = task.name
   212  		comp.HashType = artifact.HashSHA256
   213  		comp.License = pom.License
   214  
   215  		results <- result{comp: comp}
   216  	}
   217  }
   218  
   219  func GetPOM(path string) (string, error) {
   220  
   221  	if strings.ToLower(filepath.Base(path)) == pom {
   222  		return path, nil
   223  	}
   224  
   225  	fm := strings.ToLower(filepath.Join(path, pom))
   226  	if _, err := os.Stat(fm); err == nil {
   227  		return fm, nil
   228  	}
   229  
   230  	r, err := zip.OpenReader(path)
   231  
   232  	if err != nil {
   233  		return "", err
   234  	}
   235  
   236  	defer r.Close()
   237  	for _, f := range r.File {
   238  		if filepath.Base(f.Name) != "pom.xml" {
   239  			continue
   240  		}
   241  		hash := sha256.Sum256([]byte(f.Name))
   242  		tmpDirName := base64.RawURLEncoding.EncodeToString(hash[:])
   243  		tempDir := filepath.Join(os.TempDir(), "vcn", tmpDirName)
   244  		err := os.MkdirAll(tempDir, 0755)
   245  		if err != nil {
   246  			return "", err
   247  		}
   248  		tmp, err := ioutil.TempFile(tempDir, POM_name)
   249  		if err != nil {
   250  			return "", err
   251  		}
   252  		defer tmp.Close()
   253  		rc, err := f.Open()
   254  		if err != nil {
   255  			return "", err
   256  		}
   257  		defer rc.Close()
   258  
   259  		_, err = io.Copy(tmp, rc)
   260  		if err != nil {
   261  			return "", err
   262  		}
   263  		return tmp.Name(), nil
   264  	}
   265  	return "", errors.New("no pom is founded")
   266  }
   267  
   268  type MavenConfig struct {
   269  	LocalRepo string `xml:"settings>localRepository"`
   270  }
   271  
   272  // local Maven package cache dir
   273  func MavenCacheDir() (string, error) {
   274  	command := exec.Command(maven, "-v")
   275  	out, err := command.Output()
   276  	if err != nil {
   277  		exit, ok := err.(*exec.ExitError)
   278  		if ok {
   279  			return "", fmt.Errorf("cannot get Maven config:\n%s", string(exit.Stderr))
   280  		}
   281  		return "", fmt.Errorf("cannot get Maven config: %w", err)
   282  	}
   283  
   284  	var configDir string
   285  	scanner := bufio.NewScanner(bytes.NewBuffer(out))
   286  	for scanner.Scan() {
   287  		fields := strings.SplitN(scanner.Text(), ": ", 2)
   288  		if fields[0] == "Maven home" {
   289  			configDir = fields[1]
   290  			break
   291  		}
   292  	}
   293  
   294  	if configDir == "" {
   295  		return "", fmt.Errorf("cannot find Maven home dir")
   296  	}
   297  
   298  	buf, err := ioutil.ReadFile(filepath.Join(configDir, "conf", "settings.xml"))
   299  	if err != nil {
   300  		return "", fmt.Errorf("cannot read Maven config: %w", err)
   301  	}
   302  
   303  	var mvnConfig MavenConfig
   304  	err = xml.Unmarshal(buf, &mvnConfig)
   305  	if err != nil {
   306  		return "", fmt.Errorf("cannot parse Maven config: %w", err)
   307  	}
   308  
   309  	if mvnConfig.LocalRepo != "" {
   310  		return mvnConfig.LocalRepo, nil
   311  	}
   312  
   313  	// local repo path is not overridden in Maven config - use default path
   314  	home, err := os.UserHomeDir()
   315  	if err != nil {
   316  		return "", fmt.Errorf("cannot get user home dir: %w", err)
   317  	}
   318  
   319  	return filepath.Join(home, ".m2", "repository"), nil
   320  }