github.com/google/osv-scalibr@v0.4.1/extractor/filesystem/language/swift/packageresolved/packageresolved.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 packageresolved extracts Package.resolved files
    16  package packageresolved
    17  
    18  import (
    19  	"context"
    20  	"encoding/json"
    21  	"fmt"
    22  	"io"
    23  	"path/filepath"
    24  
    25  	"github.com/google/osv-scalibr/extractor"
    26  	"github.com/google/osv-scalibr/extractor/filesystem"
    27  	"github.com/google/osv-scalibr/extractor/filesystem/internal/units"
    28  	"github.com/google/osv-scalibr/inventory"
    29  	"github.com/google/osv-scalibr/plugin"
    30  	"github.com/google/osv-scalibr/purl"
    31  	"github.com/google/osv-scalibr/stats"
    32  )
    33  
    34  const (
    35  	// Name is the unique name of this extractor.
    36  	Name = "swift/packageresolved"
    37  )
    38  
    39  // Config is the configuration for the Extractor.
    40  type Config struct {
    41  	// Stats is a stats collector for reporting metrics.
    42  	Stats stats.Collector
    43  	// MaxFileSizeBytes is the maximum file size this extractor will process.
    44  	MaxFileSizeBytes int64
    45  }
    46  
    47  // DefaultConfig returns the default configuration for the extractor.
    48  func DefaultConfig() Config {
    49  	return Config{
    50  		Stats:            nil,
    51  		MaxFileSizeBytes: 10 * units.MiB,
    52  	}
    53  }
    54  
    55  // Extractor extracts packages from inside a Package.resolved.
    56  type Extractor struct {
    57  	stats            stats.Collector
    58  	maxFileSizeBytes int64
    59  }
    60  
    61  // New returns a Package.resolved extractor.
    62  func New(cfg Config) *Extractor {
    63  	return &Extractor{
    64  		stats:            cfg.Stats,
    65  		maxFileSizeBytes: cfg.MaxFileSizeBytes,
    66  	}
    67  }
    68  
    69  // NewDefault returns an extractor with the default config settings.
    70  func NewDefault() filesystem.Extractor { return New(DefaultConfig()) }
    71  
    72  // Config returns the configuration of the extractor.
    73  func (e Extractor) Config() Config {
    74  	return Config{
    75  		Stats:            e.stats,
    76  		MaxFileSizeBytes: e.maxFileSizeBytes,
    77  	}
    78  }
    79  
    80  // Name of the extractor.
    81  func (e Extractor) Name() string { return Name }
    82  
    83  // Version of the extractor.
    84  func (e Extractor) Version() int { return 0 }
    85  
    86  // Requirements of the extractor.
    87  func (e Extractor) Requirements() *plugin.Capabilities { return &plugin.Capabilities{} }
    88  
    89  // FileRequired checks if the file is named "Package.resolved".
    90  func (e Extractor) FileRequired(api filesystem.FileAPI) bool {
    91  	path := api.Path()
    92  	if filepath.Base(path) != "Package.resolved" {
    93  		return false
    94  	}
    95  
    96  	fileinfo, err := api.Stat()
    97  	if err != nil {
    98  		return false
    99  	}
   100  	if e.maxFileSizeBytes > 0 && fileinfo.Size() > e.maxFileSizeBytes {
   101  		e.reportFileRequired(path, fileinfo.Size(), stats.FileRequiredResultSizeLimitExceeded)
   102  		return false
   103  	}
   104  
   105  	e.reportFileRequired(path, fileinfo.Size(), stats.FileRequiredResultOK)
   106  	return true
   107  }
   108  
   109  func (e Extractor) reportFileRequired(path string, fileSizeBytes int64, result stats.FileRequiredResult) {
   110  	if e.stats == nil {
   111  		return
   112  	}
   113  	e.stats.AfterFileRequired(e.Name(), &stats.FileRequiredStats{
   114  		Path:          path,
   115  		Result:        result,
   116  		FileSizeBytes: fileSizeBytes,
   117  	})
   118  }
   119  
   120  // Extract parses and extracts dependency data from a Package.resolved file.
   121  func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) (inventory.Inventory, error) {
   122  	pkgs, err := e.extractFromInput(input)
   123  	if e.stats != nil {
   124  		var fileSizeBytes int64
   125  		if input.Info != nil {
   126  			fileSizeBytes = input.Info.Size()
   127  		}
   128  		e.stats.AfterFileExtracted(e.Name(), &stats.FileExtractedStats{
   129  			Path:          input.Path,
   130  			Result:        filesystem.ExtractorErrorToFileExtractedResult(err),
   131  			FileSizeBytes: fileSizeBytes,
   132  		})
   133  	}
   134  	return inventory.Inventory{Packages: pkgs}, err
   135  }
   136  
   137  func (e Extractor) extractFromInput(input *filesystem.ScanInput) ([]*extractor.Package, error) {
   138  	packages, err := parse(input.Reader)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  
   143  	var result []*extractor.Package
   144  	for _, pkg := range packages {
   145  		result = append(result, &extractor.Package{
   146  			Name:     pkg.Name,
   147  			Version:  pkg.Version,
   148  			PURLType: purl.TypeCocoapods,
   149  			Locations: []string{
   150  				input.Path,
   151  			},
   152  		})
   153  	}
   154  
   155  	return result, nil
   156  }
   157  
   158  // pkg represents a parsed package entry from the Package.resolved file.
   159  type pkg struct {
   160  	Name    string
   161  	Version string
   162  }
   163  
   164  // Parse reads and parses a Package.resolved file for package details.
   165  func parse(r io.Reader) ([]pkg, error) {
   166  	var resolvedFile struct {
   167  		Pins []struct {
   168  			Package string `json:"identity"`
   169  			State   struct {
   170  				Version string `json:"version"`
   171  			} `json:"state"`
   172  		} `json:"pins"`
   173  	}
   174  
   175  	if err := json.NewDecoder(r).Decode(&resolvedFile); err != nil {
   176  		return nil, fmt.Errorf("failed to parse Package.resolved: %w", err)
   177  	}
   178  
   179  	var packages []pkg
   180  	for _, pin := range resolvedFile.Pins {
   181  		packages = append(packages, pkg{
   182  			Name:    pin.Package,
   183  			Version: pin.State.Version,
   184  		})
   185  	}
   186  
   187  	return packages, nil
   188  }