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{}