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 }