Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(rust): Support workspace.members parsing for Cargo.toml analysis #5285

Merged
merged 15 commits into from
Jan 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 63 additions & 34 deletions pkg/fanal/analyzer/language/rust/cargo/cargo.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"io"
"io/fs"
"os"
"path"
"path/filepath"
"sort"

Expand Down Expand Up @@ -58,18 +59,18 @@ func (a cargoAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAnalysi
return filepath.Base(path) == types.CargoLock
}

err := fsutils.WalkDir(input.FS, ".", required, func(path string, d fs.DirEntry, r io.Reader) error {
err := fsutils.WalkDir(input.FS, ".", required, func(filePath string, d fs.DirEntry, r io.Reader) error {
// Parse Cargo.lock
app, err := a.parseCargoLock(path, r)
app, err := a.parseCargoLock(filePath, r)
if err != nil {
return xerrors.Errorf("parse error: %w", err)
} else if app == nil {
return nil
}

// Parse Cargo.toml alongside Cargo.lock to identify the direct dependencies
if err = a.removeDevDependencies(input.FS, filepath.Dir(path), app); err != nil {
log.Logger.Warnf("Unable to parse %q to identify direct dependencies: %s", filepath.Join(filepath.Dir(path), types.CargoToml), err)
if err = a.removeDevDependencies(input.FS, path.Dir(filePath), app); err != nil {
log.Logger.Warnf("Unable to parse %q to identify direct dependencies: %s", path.Join(path.Dir(filePath), types.CargoToml), err)
}
sort.Sort(app.Libraries)
apps = append(apps, *app)
Expand Down Expand Up @@ -98,13 +99,13 @@ func (a cargoAnalyzer) Version() int {
return version
}

func (a cargoAnalyzer) parseCargoLock(path string, r io.Reader) (*types.Application, error) {
return language.Parse(types.Cargo, path, r, a.lockParser)
func (a cargoAnalyzer) parseCargoLock(filePath string, r io.Reader) (*types.Application, error) {
return language.Parse(types.Cargo, filePath, r, a.lockParser)
}

func (a cargoAnalyzer) removeDevDependencies(fsys fs.FS, dir string, app *types.Application) error {
cargoTOMLPath := filepath.Join(dir, types.CargoToml)
directDeps, err := a.parseCargoTOML(fsys, cargoTOMLPath)
cargoTOMLPath := path.Join(dir, types.CargoToml)
directDeps, err := a.parseRootCargoTOML(fsys, cargoTOMLPath)
if errors.Is(err, fs.ErrNotExist) {
log.Logger.Debugf("Cargo: %s not found", cargoTOMLPath)
return nil
Expand Down Expand Up @@ -155,40 +156,38 @@ func (a cargoAnalyzer) removeDevDependencies(fsys fs.FS, dir string, app *types.
type cargoToml struct {
Dependencies Dependencies `toml:"dependencies"`
Target map[string]map[string]Dependencies `toml:"target"`
Workspace map[string]Dependencies `toml:"workspace"`
Workspace cargoTomlWorkspace `toml:"workspace"`
}

type cargoTomlWorkspace struct {
Dependencies Dependencies `toml:"dependencies"`
Members []string `toml:"members"`
}

type Dependencies map[string]interface{}

func (a cargoAnalyzer) parseCargoTOML(fsys fs.FS, path string) (map[string]string, error) {
// Parse Cargo.json
f, err := fsys.Open(path)
// parseRootCargoTOML parses top-level Cargo.toml and returns dependencies.
// It also parses workspace members and their dependencies.
func (a cargoAnalyzer) parseRootCargoTOML(fsys fs.FS, filePath string) (map[string]string, error) {
dependencies, members, err := parseCargoTOML(fsys, filePath)
if err != nil {
return nil, xerrors.Errorf("file open error: %w", err)
return nil, xerrors.Errorf("unable to parse %s: %w", filePath, err)
}
defer func() { _ = f.Close() }()

tomlFile := cargoToml{}
deps := make(map[string]string)
_, err = toml.NewDecoder(f).Decode(&tomlFile)
if err != nil {
return nil, xerrors.Errorf("toml decode error: %w", err)
}

// There are cases when toml file doesn't include `Dependencies` field (then map will be nil).
// e.g. when only `workspace.Dependencies` are used
// declare `dependencies` to avoid panic
dependencies := Dependencies{}
maps.Copy(dependencies, tomlFile.Dependencies)

// https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies
for _, target := range tomlFile.Target {
maps.Copy(dependencies, target["dependencies"])
// According to Cargo workspace RFC, workspaces can't be nested:
// https://github.com/nox/rust-rfcs/blob/master/text/1525-cargo-workspace.md#validating-a-workspace
for _, member := range members {
memberPath := path.Join(path.Dir(filePath), member, types.CargoToml)
memberDeps, _, err := parseCargoTOML(fsys, memberPath)
if err != nil {
log.Logger.Warnf("Unable to parse %q: %s", memberPath, err)
continue
}
// Member dependencies shouldn't overwrite dependencies from root cargo.toml file
maps.Copy(memberDeps, dependencies)
dependencies = memberDeps
}

// https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#inheriting-a-dependency-from-a-workspace
maps.Copy(dependencies, tomlFile.Workspace["dependencies"])

deps := make(map[string]string)
for name, value := range dependencies {
switch ver := value.(type) {
case string:
Expand Down Expand Up @@ -250,3 +249,33 @@ func (a cargoAnalyzer) matchVersion(currentVersion, constraint string) (bool, er

return c.Check(ver), nil
}

func parseCargoTOML(fsys fs.FS, filePath string) (Dependencies, []string, error) {
// Parse Cargo.toml
f, err := fsys.Open(filePath)
if err != nil {
return nil, nil, xerrors.Errorf("file open error: %w", err)
}
defer func() { _ = f.Close() }()

var tomlFile cargoToml
// There are cases when toml file doesn't include `Dependencies` field (then map will be nil).
// e.g. when only `workspace.Dependencies` are used
// declare `dependencies` to avoid panic
dependencies := Dependencies{}
if _, err = toml.NewDecoder(f).Decode(&tomlFile); err != nil {
return nil, nil, xerrors.Errorf("toml decode error: %w", err)
}

maps.Copy(dependencies, tomlFile.Dependencies)

// https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#platform-specific-dependencies
for _, target := range tomlFile.Target {
maps.Copy(dependencies, target["dependencies"])
}

// https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#inheriting-a-dependency-from-a-workspace
maps.Copy(dependencies, tomlFile.Workspace.Dependencies)
// https://doc.rust-lang.org/cargo/reference/workspaces.html#the-members-and-exclude-fields
return dependencies, tomlFile.Workspace.Members, nil
}
126 changes: 126 additions & 0 deletions pkg/fanal/analyzer/language/rust/cargo/cargo_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,132 @@ func Test_cargoAnalyzer_Analyze(t *testing.T) {
dir: "testdata/sad",
want: &analyzer.AnalysisResult{},
},
{
name: "workspace members",
dir: "testdata/toml-workspace-members",
want: &analyzer.AnalysisResult{
Applications: []types.Application{
{
Type: types.Cargo,
FilePath: "Cargo.lock",
Libraries: types.Packages{
{
ID: "aho-corasick@1.1.2",
Name: "aho-corasick",
Version: "1.1.2",
Indirect: true,
Locations: []types.Location{
{
StartLine: 5,
EndLine: 12,
},
},
DependsOn: []string{"memchr@2.6.4"},
},
{
ID: "gdb-command@0.7.6",
Name: "gdb-command",
Version: "0.7.6",
Indirect: false,
DmitriyLewen marked this conversation as resolved.
Show resolved Hide resolved
Locations: []types.Location{
{
StartLine: 14,
EndLine: 22,
},
},
DependsOn: []string{
"regex@1.10.2",
"wait-timeout@0.2.0",
},
},
{
ID: "libc@0.2.150",
Name: "libc",
Version: "0.2.150",
Indirect: true,
Locations: []types.Location{
{
StartLine: 24,
EndLine: 28,
},
},
},
{
ID: "memchr@2.6.4",
Name: "memchr",
Version: "2.6.4",
Indirect: true,
Locations: []types.Location{
{
StartLine: 44,
EndLine: 48,
},
},
},
{
ID: "regex@1.10.2",
Name: "regex",
Version: "1.10.2",
Locations: []types.Location{
{
StartLine: 50,
EndLine: 60,
},
},
DependsOn: []string{
"aho-corasick@1.1.2",
"memchr@2.6.4",
"regex-automata@0.4.3",
"regex-syntax@0.8.2",
},
},
{
ID: "regex-automata@0.4.3",
Name: "regex-automata",
Version: "0.4.3",
Indirect: true,
Locations: []types.Location{
{
StartLine: 62,
EndLine: 71,
},
},
DependsOn: []string{
"aho-corasick@1.1.2",
"memchr@2.6.4",
"regex-syntax@0.8.2",
},
},
{
ID: "regex-syntax@0.8.2",
Name: "regex-syntax",
Version: "0.8.2",
Indirect: true,
Locations: []types.Location{
{
StartLine: 73,
EndLine: 77,
},
},
},
{
ID: "wait-timeout@0.2.0",
Name: "wait-timeout",
Version: "0.2.0",
Indirect: true,
Locations: []types.Location{
{
StartLine: 79,
EndLine: 86,
},
},
DependsOn: []string{"libc@0.2.150"},
},
},
},
},
},
},
}

for _, tt := range tests {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[workspace.package]
name = "toml-workspace-members"
version = "0.1.0"

[workspace]
resolver = "2"
members = ["member", "member2"]

[workspace.dependencies]
regex = "1"
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "member"
version = "0.1.0"
edition = "2021"

[dependencies]
gdb-command = "0.7"
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
edition = "2021"
name = "member2"
version = "0.1.0"

[dependencies]
regex = { workspace = true}
Loading