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  }