github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/rust/cargotoml/cargotoml.go (about)

     1  // Copyright 2025 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Copyright 2025 Google LLC
    16  //
    17  // Licensed under the Apache License, Version 2.0 (the "License");
    18  // you may not use this file except in compliance with the License.
    19  // You may obtain a copy of the License at
    20  //
    21  //	http://www.apache.org/licenses/LICENSE-2.0
    22  //
    23  // Unless required by applicable law or agreed to in writing, software
    24  // distributed under the License is distributed on an "AS IS" BASIS,
    25  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    26  // See the License for the specific language governing permissions and
    27  // limitations under the License.
    28  
    29  // Package cargotoml extracts Cargo.toml files for rust projects
    30  package cargotoml
    31  
    32  import (
    33  	"context"
    34  	"errors"
    35  	"fmt"
    36  	"path/filepath"
    37  	"regexp"
    38  
    39  	"github.com/BurntSushi/toml"
    40  
    41  	"github.com/google/osv-scalibr/extractor"
    42  	"github.com/google/osv-scalibr/extractor/filesystem"
    43  	"github.com/google/osv-scalibr/inventory"
    44  	"github.com/google/osv-scalibr/plugin"
    45  	"github.com/google/osv-scalibr/purl"
    46  )
    47  
    48  const (
    49  	// Name is the name of the Extractor.
    50  	Name = "rust/cargotoml"
    51  )
    52  
    53  var shaPattern = regexp.MustCompile("^[0-9a-f]{40}$")
    54  
    55  type cargoTomlDependency struct {
    56  	Version string
    57  	Git     string
    58  	Rev     string
    59  }
    60  
    61  // UnmarshalTOML parses a dependency from a Cargo.toml file.
    62  //
    63  // Dependencies in Cargo.toml can be defined as simple strings (e.g., version)
    64  // or as more complex objects (e.g., with version, path, etc.)
    65  //
    66  // in case both the Version and Git/Path are specified the version should be considered
    67  // the source of truth
    68  func (v *cargoTomlDependency) UnmarshalTOML(data any) error {
    69  	getString := func(m map[string]any, key string) (string, error) {
    70  		v, ok := m[key]
    71  		if !ok {
    72  			// if the key does not exists leave the string value empty
    73  			return "", nil
    74  		}
    75  		s, ok := v.(string)
    76  		if !ok {
    77  			// if the key exists but the type is wrong return an error
    78  			return "", fmt.Errorf("invalid type for key %q: expected string, got %T", key, v)
    79  		}
    80  		return s, nil
    81  	}
    82  
    83  	switch data := data.(type) {
    84  	case string:
    85  		// if the type is string then the data is version
    86  		v.Version = data
    87  		return nil
    88  	case map[string]any:
    89  		var err error
    90  		if v.Version, err = getString(data, "version"); err != nil {
    91  			return err
    92  		}
    93  		if v.Git, err = getString(data, "git"); err != nil {
    94  			return err
    95  		}
    96  		if v.Rev, err = getString(data, "rev"); err != nil {
    97  			return err
    98  		}
    99  		return nil
   100  	default:
   101  		return errors.New("invalid format for Cargo.toml dependency")
   102  	}
   103  }
   104  
   105  // IsCommitSpecified checks if the dependency specifies a Git commit.
   106  func (v *cargoTomlDependency) IsCommitSpecified() bool {
   107  	return v.Git != "" && shaPattern.MatchString(v.Rev)
   108  }
   109  
   110  type cargoTomlPackage struct {
   111  	Name    string `toml:"name"`
   112  	Version string `toml:"version"`
   113  }
   114  
   115  type cargoTomlFile struct {
   116  	Package      cargoTomlPackage               `toml:"package"`
   117  	Dependencies map[string]cargoTomlDependency `toml:"dependencies"`
   118  }
   119  
   120  // Extractor extracts crates.io packages from Cargo.toml files.
   121  type Extractor struct{}
   122  
   123  // New returns a new instance of the extractor.
   124  func New() filesystem.Extractor { return &Extractor{} }
   125  
   126  // Name of the extractor
   127  func (e Extractor) Name() string { return Name }
   128  
   129  // Version of the extractor
   130  func (e Extractor) Version() int { return 0 }
   131  
   132  // FileRequired returns true if the specified file matches Cargo toml file patterns.
   133  func (e Extractor) FileRequired(api filesystem.FileAPI) bool {
   134  	return filepath.Base(api.Path()) == "Cargo.toml"
   135  }
   136  
   137  // Requirements of the extractor
   138  func (e Extractor) Requirements() *plugin.Capabilities {
   139  	return &plugin.Capabilities{}
   140  }
   141  
   142  // Extract extracts packages from Cargo.toml files passed through the scan input.
   143  func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) {
   144  	var parsedTomlFile cargoTomlFile
   145  
   146  	_, err := toml.NewDecoder(input.Reader).Decode(&parsedTomlFile)
   147  	if err != nil {
   148  		return inventory.Inventory{}, fmt.Errorf("could not extract: %w", err)
   149  	}
   150  
   151  	packages := make([]*extractor.Package, 0, len(parsedTomlFile.Dependencies)+1)
   152  
   153  	packages = append(packages, &extractor.Package{
   154  		Name:      parsedTomlFile.Package.Name,
   155  		Version:   parsedTomlFile.Package.Version,
   156  		PURLType:  purl.TypeCargo,
   157  		Locations: []string{input.Path},
   158  	})
   159  
   160  	for name, dependency := range parsedTomlFile.Dependencies {
   161  		if err := ctx.Err(); err != nil {
   162  			return inventory.Inventory{Packages: packages}, fmt.Errorf("%s halted due to context error: %w", e.Name(), err)
   163  		}
   164  
   165  		var srcCode *extractor.SourceCodeIdentifier
   166  		if dependency.IsCommitSpecified() {
   167  			srcCode = &extractor.SourceCodeIdentifier{
   168  				Repo:   dependency.Git,
   169  				Commit: dependency.Rev,
   170  			}
   171  		}
   172  
   173  		// Skip dependencies that have no version and no useful source code information
   174  		if dependency.Version == "" && srcCode == nil {
   175  			continue
   176  		}
   177  
   178  		packages = append(packages, &extractor.Package{
   179  			Name:       name,
   180  			Version:    dependency.Version,
   181  			PURLType:   purl.TypeCargo,
   182  			Locations:  []string{input.Path},
   183  			SourceCode: srcCode,
   184  		})
   185  	}
   186  
   187  	return inventory.Inventory{Packages: packages}, nil
   188  }
   189  
   190  var _ filesystem.Extractor = Extractor{}