github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/dart/pubspec/pubspec.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 pubspec extracts Dart pubspec.lock files.
    16  package pubspec
    17  
    18  import (
    19  	"context"
    20  	"fmt"
    21  	"path/filepath"
    22  	"slices"
    23  	"strings"
    24  
    25  	"github.com/google/osv-scalibr/extractor"
    26  	"github.com/google/osv-scalibr/extractor/filesystem"
    27  	"github.com/google/osv-scalibr/extractor/filesystem/osv"
    28  	"github.com/google/osv-scalibr/inventory"
    29  	"github.com/google/osv-scalibr/plugin"
    30  	"github.com/google/osv-scalibr/purl"
    31  	"gopkg.in/yaml.v3"
    32  )
    33  
    34  const (
    35  	// Name is the unique name of this extractor.
    36  	Name = "dart/pubspec"
    37  )
    38  
    39  type pubspecLockDescription struct {
    40  	Ref string `yaml:"resolved-ref"`
    41  }
    42  
    43  var _ yaml.Unmarshaler = &pubspecLockDescription{}
    44  
    45  // UnmarshalYAML is a custom unmarshalling function for pubspecLockDescription.
    46  // We need this because descriptions can have two different formats.
    47  func (pld *pubspecLockDescription) UnmarshalYAML(value *yaml.Node) error {
    48  	// Duplicating the struct to decode nested fields as a
    49  	// workaround for https://github.com/go-yaml/yaml/issues/1000
    50  	var m struct {
    51  		Ref string `yaml:"resolved-ref"`
    52  	}
    53  	if err := value.Decode(&m); err == nil {
    54  		pld.Ref = m.Ref
    55  		return nil
    56  	}
    57  
    58  	// If the above failed, the description is a single name string with no ref.
    59  	return nil
    60  }
    61  
    62  type pubspecLockPackage struct {
    63  	Description pubspecLockDescription `yaml:"description"`
    64  	Version     string                 `yaml:"version"`
    65  	Dependency  string                 `yaml:"dependency"`
    66  }
    67  
    68  type pubspecLockfile struct {
    69  	Packages map[string]pubspecLockPackage `yaml:"packages,omitempty"`
    70  }
    71  
    72  // Extractor extracts Dart pubspec.lock files
    73  type Extractor struct{}
    74  
    75  // New returns a new instance of this Extractor.
    76  func New() filesystem.Extractor { return &Extractor{} }
    77  
    78  // Name of the extractor
    79  func (e Extractor) Name() string { return Name }
    80  
    81  // Version of the extractor
    82  func (e Extractor) Version() int { return 0 }
    83  
    84  // Requirements of the extractor
    85  func (e Extractor) Requirements() *plugin.Capabilities {
    86  	return &plugin.Capabilities{}
    87  }
    88  
    89  // FileRequired returns true if the specified file is a pubspec.lock
    90  func (e Extractor) FileRequired(api filesystem.FileAPI) bool {
    91  	return filepath.Base(api.Path()) == "pubspec.lock"
    92  }
    93  
    94  // Extract extracts Dart packages from pubspec.lock files passed through the input.
    95  func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) {
    96  	var parsedLockfile *pubspecLockfile
    97  	if err := yaml.NewDecoder(input.Reader).Decode(&parsedLockfile); err != nil {
    98  		return inventory.Inventory{}, fmt.Errorf("could not extract: %w", err)
    99  	}
   100  
   101  	packages := make([]*extractor.Package, 0, len(parsedLockfile.Packages))
   102  
   103  	for name, pkg := range parsedLockfile.Packages {
   104  		pkgDetails := &extractor.Package{
   105  			Name:      name,
   106  			Version:   pkg.Version,
   107  			PURLType:  purl.TypePub,
   108  			Locations: []string{input.Path},
   109  			SourceCode: &extractor.SourceCodeIdentifier{
   110  				Commit: pkg.Description.Ref,
   111  			},
   112  			Metadata: osv.DepGroupMetadata{},
   113  		}
   114  		if slices.Contains(strings.Split(pkg.Dependency, " "), "dev") {
   115  			pkgDetails.Metadata = osv.DepGroupMetadata{DepGroupVals: []string{"dev"}}
   116  		}
   117  		packages = append(packages, pkgDetails)
   118  	}
   119  
   120  	return inventory.Inventory{Packages: packages}, nil
   121  }
   122  
   123  var _ filesystem.Extractor = Extractor{}