github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/runtime/nodejs/nvm/nvm.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 // Package nvm extracts the Node.js version from nvm .nvmrc files. 16 package nvm 17 18 import ( 19 "bufio" 20 "context" 21 "fmt" 22 "path" 23 "regexp" 24 "strings" 25 26 "github.com/google/osv-scalibr/extractor" 27 "github.com/google/osv-scalibr/extractor/filesystem" 28 nvmmeta "github.com/google/osv-scalibr/extractor/filesystem/runtime/nodejs/nvm/metadata" 29 "github.com/google/osv-scalibr/inventory" 30 "github.com/google/osv-scalibr/plugin" 31 "github.com/google/osv-scalibr/purl" 32 ) 33 34 const ( 35 // Name is the unique name of this extractor. 36 Name = "runtime/nvm" 37 ) 38 39 // Extractor extracts nvm Node.js versions. 40 type Extractor struct{} 41 42 // New returns a new instance of the extractor. 43 func New() filesystem.Extractor { return &Extractor{} } 44 45 // Name of the extractor. 46 func (e Extractor) Name() string { return Name } 47 48 // Version of the extractor. 49 func (e Extractor) Version() int { return 0 } 50 51 // Requirements of the extractor. 52 func (e Extractor) Requirements() *plugin.Capabilities { 53 return &plugin.Capabilities{} 54 } 55 56 // FileRequired returns true if the file name is '.nvmrc'. 57 func (e Extractor) FileRequired(api filesystem.FileAPI) bool { 58 return path.Base(api.Path()) == ".nvmrc" 59 } 60 61 func parseVersionLine(line string) (version string, ok bool) { 62 line = strings.TrimSpace(line) 63 // Comments and empty lines are ignored 64 if line == "" || strings.HasPrefix(line, "#") { 65 return "", false 66 } 67 // Remove 'v' prefix if present (e.g., v18.17.0 -> 18.17.0) 68 version = strings.TrimPrefix(line, "v") 69 // Skip if the version doesn't start with a digit. 70 // This is for skipping special keywords like 'lts/*', 'node', 'system'. 71 var startDigitRE = regexp.MustCompile(`^[0-9]`) 72 if !startDigitRE.MatchString(version) { 73 return "", false 74 } 75 return version, true 76 } 77 78 // Extract extracts Node.js version from the nvm .nvmrc file. 79 // 80 // Reference: https://github.com/nvm-sh/nvm#nvmrc 81 func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) { 82 scanner := bufio.NewScanner(input.Reader) 83 var pkgs []*extractor.Package 84 85 // NVM .nvmrc files typically contain a single Node.js version, 86 // But we'll read all lines in case there are comments or multiple versions 87 for scanner.Scan() { 88 if err := ctx.Err(); err != nil { 89 return inventory.Inventory{}, fmt.Errorf("%s halted due to context error: %w", e.Name(), err) 90 } 91 92 version, ok := parseVersionLine(scanner.Text()) 93 if !ok { 94 continue 95 } 96 97 pkgs = append(pkgs, &extractor.Package{ 98 Name: "nodejs", 99 Version: version, 100 PURLType: purl.TypeGeneric, 101 Locations: []string{input.Path}, 102 Metadata: &nvmmeta.Metadata{ 103 NodeJsVersion: version, 104 }, 105 }) 106 // For nvm, we typically only expect one version per file 107 break 108 } 109 110 if err := scanner.Err(); err != nil { 111 return inventory.Inventory{}, err 112 } 113 return inventory.Inventory{Packages: pkgs}, nil 114 }