Add rust cargo support for dep command. (#121)

* Improve license comparation with operator.
* Add rust cargo support.
diff --git a/Dockerfile b/Dockerfile
index 1df4281..8024a92 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -32,7 +32,7 @@
 # Go
 COPY --from=build /usr/local/go/bin/go /usr/local/go/bin/go
 ENV PATH="/usr/local/go/bin:$PATH"
-RUN apk add --no-cache bash gcc musl-dev npm
+RUN apk add --no-cache bash gcc musl-dev npm cargo
 # Go
 
 WORKDIR /github/workspace/
diff --git a/pkg/deps/cargo.go b/pkg/deps/cargo.go
new file mode 100644
index 0000000..e2613d2
--- /dev/null
+++ b/pkg/deps/cargo.go
@@ -0,0 +1,158 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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
+//
+//     http://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 deps
+
+import (
+	"encoding/json"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"regexp"
+
+	"github.com/apache/skywalking-eyes/internal/logger"
+	"github.com/apache/skywalking-eyes/pkg/license"
+)
+
+type CargoMetadata struct {
+	Packages []CargoPackage `json:"packages"`
+}
+
+type CargoPackage struct {
+	Name         string `json:"name"`
+	Version      string `json:"version"`
+	License      string `json:"license"`
+	LicenseFile  string `json:"license_file"`
+	ManifestPath string `json:"manifest_path"`
+}
+
+type CargoTomlResolver struct {
+	Resolver
+}
+
+func (resolver *CargoTomlResolver) CanResolve(file string) bool {
+	base := filepath.Base(file)
+	logger.Log.Debugln("Base name:", base)
+	return base == "Cargo.toml"
+}
+
+// Resolve resolves licenses of all dependencies declared in the Cargo.toml file.
+func (resolver *CargoTomlResolver) Resolve(cargoTomlFile string, config *ConfigDeps, report *Report) error {
+	dir := filepath.Dir(cargoTomlFile)
+
+	download := exec.Command("cargo", "fetch")
+	logger.Log.Debugf("Run command: %v, please wait", download.String())
+	download.Stdout = os.Stdout
+	download.Stderr = os.Stderr
+	download.Dir = dir
+	if err := download.Run(); err != nil {
+		return err
+	}
+
+	cmd := exec.Command("cargo", "metadata", "--format-version=1", "--all-features")
+	cmd.Dir = dir
+	output, err := cmd.Output()
+	if err != nil {
+		return err
+	}
+
+	var metadata CargoMetadata
+	if err := json.Unmarshal(output, &metadata); err != nil {
+		return err
+	}
+
+	logger.Log.Debugln("Package size:", len(metadata.Packages))
+
+	return resolver.ResolvePackages(metadata.Packages, config, report)
+}
+
+// ResolvePackages resolves the licenses of the given packages.
+func (resolver *CargoTomlResolver) ResolvePackages(packages []CargoPackage, config *ConfigDeps, report *Report) error {
+	for i := range packages {
+		pkg := packages[i]
+
+		if config.IsExcluded(pkg.Name, pkg.Version) {
+			continue
+		}
+		if l, ok := config.GetUserConfiguredLicense(pkg.Name, pkg.Version); ok {
+			report.Resolve(&Result{
+				Dependency:    pkg.Name,
+				LicenseSpdxID: l,
+				Version:       pkg.Version,
+			})
+			continue
+		}
+		err := resolver.ResolvePackageLicense(config, &pkg, report)
+		if err != nil {
+			logger.Log.Warnf("Failed to resolve the license of <%s@%s>: %v\n", pkg.Name, pkg.Version, err)
+			report.Skip(&Result{
+				Dependency:    pkg.Name,
+				LicenseSpdxID: Unknown,
+				Version:       pkg.Version,
+			})
+		}
+	}
+	return nil
+}
+
+var cargoPossibleLicenseFileName = regexp.MustCompile(`(?i)^LICENSE|LICENCE(\.txt)?|LICENSE-.+|COPYING(\.txt)?$`)
+
+// ResolvePackageLicense resolve the package license.
+// The CargoPackage.LicenseFile is generally used for non-standard licenses and is ignored now.
+func (resolver *CargoTomlResolver) ResolvePackageLicense(config *ConfigDeps, pkg *CargoPackage, report *Report) error {
+	dir := filepath.Dir(pkg.ManifestPath)
+	logger.Log.Debugf("Directory of %+v is %+v", pkg.Name, dir)
+	files, err := os.ReadDir(dir)
+	if err != nil {
+		return nil
+	}
+
+	var licenseFilePath string
+	var licenseContent []byte
+
+	licenseID := pkg.License
+
+	for _, info := range files {
+		if !cargoPossibleLicenseFileName.MatchString(info.Name()) {
+			continue
+		}
+
+		licenseFilePath = filepath.Join(dir, info.Name())
+		licenseContent, err = os.ReadFile(licenseFilePath)
+		if err != nil {
+			return err
+		}
+
+		break
+	}
+
+	if licenseID == "" { // If pkg.License is empty, identify the license ID from the license file content
+		if licenseID, err = license.Identify(string(licenseContent), config.Threshold); err != nil {
+			return err
+		}
+	}
+
+	report.Resolve(&Result{
+		Dependency:      pkg.Name,
+		LicenseFilePath: licenseFilePath,
+		LicenseContent:  string(licenseContent),
+		LicenseSpdxID:   licenseID,
+		Version:         pkg.Version,
+	})
+
+	return nil
+}
diff --git a/pkg/deps/cargo_test.go b/pkg/deps/cargo_test.go
new file mode 100644
index 0000000..db401a3
--- /dev/null
+++ b/pkg/deps/cargo_test.go
@@ -0,0 +1,198 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The ASF licenses this file
+// to you 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
+//
+//     http://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 deps_test
+
+import (
+	"github.com/apache/skywalking-eyes/internal/logger"
+	"github.com/apache/skywalking-eyes/pkg/deps"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"testing"
+)
+
+func TestCanResolveCargo(t *testing.T) {
+	resolver := new(deps.CargoTomlResolver)
+	if !resolver.CanResolve("Cargo.toml") {
+		t.Error("CargoTomlResolver should resolve Cargo.toml")
+		return
+	}
+	if resolver.CanResolve("go.mod") {
+		t.Error("CargoTomlResolver shouldn't resolve go.mod")
+	}
+}
+
+func TestResolveCargos(t *testing.T) {
+	if _, err := exec.Command("cargo", "--version").Output(); err != nil {
+		logger.Log.Warnf("Failed to find cargo, the test `TestResolveCargo` was skipped")
+		return
+	}
+
+	{
+		cargoToml := `
+[package]
+name = "foo"
+version = "0.0.0"
+publish = false
+edition = "2021"
+license = "Apache-2.0"
+`
+
+		config := deps.ConfigDeps{
+			Threshold: 0,
+			Files:     []string{"Cargo.toml"},
+			Licenses:  []*deps.ConfigDepLicense{},
+			Excludes:  []deps.Exclude{},
+		}
+
+		report := resolveTmpCargo(t, cargoToml, &config)
+		if len(report.Resolved) != 1 {
+			t.Error("len(report.Resolved) != 1")
+		}
+		if report.Resolved[0].LicenseSpdxID != "Apache-2.0" {
+			t.Error("Package foo license isn't Apache-2.0")
+		}
+	}
+
+	{
+		cargoToml := `
+[package]
+name = "foo"
+version = "0.0.0"
+publish = false
+edition = "2021"
+license = "Apache-2.0"
+`
+
+		config := deps.ConfigDeps{
+			Threshold: 0,
+			Files:     []string{"Cargo.toml"},
+			Licenses:  []*deps.ConfigDepLicense{},
+			Excludes:  []deps.Exclude{{Name: "foo", Version: "0.0.0"}},
+		}
+
+		report := resolveTmpCargo(t, cargoToml, &config)
+		if len(report.Resolved) != 0 {
+			t.Error("len(report.Resolved) != 0")
+		}
+	}
+
+	{
+		cargoToml := `
+[package]
+name = "foo"
+version = "0.0.0"
+publish = false
+edition = "2021"
+license = "Apache-2.0"
+`
+
+		config := deps.ConfigDeps{
+			Threshold: 0,
+			Files:     []string{},
+			Licenses: []*deps.ConfigDepLicense{
+				{
+					Name:    "foo",
+					Version: "0.0.0",
+					License: "MIT",
+				},
+			},
+			Excludes: []deps.Exclude{},
+		}
+
+		report := resolveTmpCargo(t, cargoToml, &config)
+		if len(report.Resolved) != 1 {
+			t.Error("len(report.Resolved) != 1")
+		}
+		if report.Resolved[0].LicenseSpdxID != "MIT" {
+			t.Error("Package foo license isn't modified to  MIT")
+		}
+	}
+
+	{
+		cargoToml := `
+[package]
+name = "foo"
+version = "0.0.0"
+publish = false
+edition = "2021"
+license = "Apache-2.0"
+
+[dependencies]
+libc = "0.2.126"
+`
+
+		config := deps.ConfigDeps{
+			Threshold: 0,
+			Files:     []string{"Cargo.toml"},
+			Licenses:  []*deps.ConfigDepLicense{},
+			Excludes:  []deps.Exclude{},
+		}
+
+		report := resolveTmpCargo(t, cargoToml, &config)
+		if len(report.Resolved) != 2 {
+			t.Error("len(report.Resolved) != 2")
+		}
+		for _, result := range report.Resolved {
+			if result.Dependency == "libc" {
+				if result.LicenseSpdxID != "MIT OR Apache-2.0" || result.LicenseContent == "" {
+					t.Error("Resolve dependency libc failed")
+				}
+			}
+		}
+	}
+}
+
+func resolveTmpCargo(t *testing.T, cargoTomlContent string, config *deps.ConfigDeps) *deps.Report {
+	dir, err := os.MkdirTemp("", "skywalking-eyes-test-cargo-")
+	if err != nil {
+		t.Error("Make temp dir failed", err)
+		return nil
+	}
+	defer func(path string) {
+		err := os.RemoveAll(path)
+		if err != nil {
+			logger.Log.Warn(err)
+		}
+	}(dir) // clean up
+
+	if err := os.Chdir(dir); err != nil {
+		t.Error("Chdir failed", err)
+		return nil
+	}
+
+	if _, err := exec.Command("cargo", "init", "--lib").Output(); err != nil {
+		t.Error("Cargo init failed", err)
+		return nil
+	}
+
+	cargoFile := filepath.Join(dir, "Cargo.toml")
+	if err := os.WriteFile(cargoFile, []byte(cargoTomlContent), 0644); err != nil {
+		t.Error("Write Cargo.toml failed", err)
+		return nil
+	}
+
+	resolver := new(deps.CargoTomlResolver)
+
+	var report deps.Report
+	if err := resolver.Resolve(cargoFile, config, &report); err != nil {
+		t.Error("CargoTomlResolver resolve failed", err)
+		return nil
+	}
+	return &report
+}
diff --git a/pkg/deps/resolve.go b/pkg/deps/resolve.go
index 8769d79..fc2f562 100644
--- a/pkg/deps/resolve.go
+++ b/pkg/deps/resolve.go
@@ -31,6 +31,7 @@
 	new(NpmResolver),
 	new(MavenPomResolver),
 	new(JarResolver),
+	new(CargoTomlResolver),
 }
 
 func Resolve(config *ConfigDeps, report *Report) error {