-
Notifications
You must be signed in to change notification settings - Fork 31
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add support for
bun.lock
(#379)
As part of improving ergonomics, [Bun has introduced a new text-based lockfile](https://bun.sh/blog/bun-lock-text-lockfile) which this adds support for extracting. While the blog post claims that it's "JSON with comments" (aka `JSONC`), it's actually "JSON with comments _and_ trailing commas" (aka [`JWCC`](https://nigeltao.github.io/blog/2021/json-with-commas-comments.html) or [`HuJSON`](https://github.com/tailscale/hujson)) - since the Go standard library only supports parsing standard JSON, I've had to bring in a third-party library to handle parsing, which is designed to leverage the existing standard library by instead handling "standardizing" the input into valid boring JSON. ~Aside from the general question of if this library is acceptable to use here, it also seems to require Go 1.23 resulting in the addition of a `toolchain` line in `go.mod` which I've never managed to quite figure out what the right thing to do with - overall, I'd like someone from Google to confirm what library they'd prefer we use here.~ (I've since switched to using a library that requires Go 1.16) I have also specified the testdata fixtures as JSON5 as that's the most appropriate format supported by both VSCode and IntelliJ/GoLand, though that technically supports more features like single quotes (which it actually seems like `bun` does not mind if you use in your lockfile, though it'll always use double quotes itself) - personally I think that's fine, but don't mind renaming the files to be `.hujson` if folks would prefer. Resolves google/osv-scanner#1405 Closes #379 COPYBARA_INTEGRATE_REVIEW=#379 from ackama:bun/support 0771c5a PiperOrigin-RevId: 720546722
- Loading branch information
1 parent
34aef7c
commit abf6098
Showing
25 changed files
with
1,345 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
148 changes: 148 additions & 0 deletions
148
extractor/filesystem/language/javascript/bunlock/bunlock.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,148 @@ | ||
// Copyright 2025 Google LLC | ||
// | ||
// Licensed under the Apache License, Version 2.0 (the "License"); | ||
// you may not use this file except in compliance with the License. | ||
// You may obtain a copy of the License at | ||
// | ||
// http://www.apache.org/licenses/LICENSE-2.0 | ||
// | ||
// Unless required by applicable law or agreed to in writing, software | ||
// distributed under the License is distributed on an "AS IS" BASIS, | ||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
// See the License for the specific language governing permissions and | ||
// limitations under the License. | ||
|
||
// Package bunlock extracts bun.lock files | ||
package bunlock | ||
|
||
import ( | ||
"context" | ||
"encoding/json" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"path/filepath" | ||
"strings" | ||
|
||
"github.com/google/osv-scalibr/extractor" | ||
"github.com/google/osv-scalibr/extractor/filesystem" | ||
"github.com/google/osv-scalibr/extractor/filesystem/osv" | ||
"github.com/google/osv-scalibr/plugin" | ||
"github.com/google/osv-scalibr/purl" | ||
"github.com/tidwall/jsonc/pkg/jsonc" | ||
Check failure on line 32 in extractor/filesystem/language/javascript/bunlock/bunlock.go
|
||
) | ||
|
||
type bunLockfile struct { | ||
Version int `json:"lockfileVersion"` | ||
Packages map[string][]any `json:"packages"` | ||
} | ||
|
||
// Extractor extracts npm packages from bun.lock files. | ||
type Extractor struct{} | ||
|
||
// Name of the extractor. | ||
func (e Extractor) Name() string { return "javascript/bunlock" } | ||
|
||
// Version of the extractor. | ||
func (e Extractor) Version() int { return 0 } | ||
|
||
// Requirements of the extractor. | ||
func (e Extractor) Requirements() *plugin.Capabilities { | ||
return &plugin.Capabilities{} | ||
} | ||
|
||
// FileRequired returns true if the specified file matches bun lockfile patterns. | ||
func (e Extractor) FileRequired(api filesystem.FileAPI) bool { | ||
return filepath.Base(api.Path()) == "bun.lock" | ||
} | ||
|
||
// structurePackageDetails returns the name, version, and commit of a package | ||
// specified as a tuple in a bun.lock | ||
func structurePackageDetails(pkg []any) (string, string, string, error) { | ||
if len(pkg) == 0 { | ||
return "", "", "", fmt.Errorf("empty package tuple") | ||
} | ||
|
||
str, ok := pkg[0].(string) | ||
|
||
if !ok { | ||
return "", "", "", fmt.Errorf("first element of package tuple is not a string") | ||
} | ||
|
||
str, isScoped := strings.CutPrefix(str, "@") | ||
name, version, _ := strings.Cut(str, "@") | ||
|
||
if isScoped { | ||
name = "@" + name | ||
} | ||
|
||
version, commit, _ := strings.Cut(version, "#") | ||
|
||
// bun.lock does not track both the commit and version, | ||
// so if we have a commit then we don't have a version | ||
if commit != "" { | ||
version = "" | ||
} | ||
|
||
// file dependencies do not have a semantic version recorded | ||
if strings.HasPrefix(version, "file:") { | ||
version = "" | ||
} | ||
|
||
return name, version, commit, nil | ||
} | ||
|
||
// Extract extracts packages from bun.lock files passed through the scan input. | ||
func (e Extractor) Extract(ctx context.Context, input *filesystem.ScanInput) ([]*extractor.Inventory, error) { | ||
var parsedLockfile *bunLockfile | ||
|
||
b, err := io.ReadAll(input.Reader) | ||
|
||
if err != nil { | ||
return nil, fmt.Errorf("could not extract from %q: %w", input.Path, err) | ||
} | ||
|
||
if err := json.Unmarshal(jsonc.ToJSON(b), &parsedLockfile); err != nil { | ||
return nil, fmt.Errorf("could not extract from %q: %w", input.Path, err) | ||
} | ||
|
||
inventories := make([]*extractor.Inventory, 0, len(parsedLockfile.Packages)) | ||
|
||
var errs []error | ||
|
||
for key, pkg := range parsedLockfile.Packages { | ||
name, version, commit, err := structurePackageDetails(pkg) | ||
|
||
if err != nil { | ||
errs = append(errs, fmt.Errorf("could not extract '%s' from %q: %w", key, input.Path, err)) | ||
|
||
continue | ||
} | ||
|
||
inventories = append(inventories, &extractor.Inventory{ | ||
Name: name, | ||
Version: version, | ||
SourceCode: &extractor.SourceCodeIdentifier{ | ||
Commit: commit, | ||
}, | ||
Metadata: osv.DepGroupMetadata{ | ||
DepGroupVals: []string{}, | ||
}, | ||
Locations: []string{input.Path}, | ||
}) | ||
} | ||
|
||
return inventories, errors.Join(errs...) | ||
} | ||
|
||
// ToPURL converts an inventory created by this extractor into a PURL. | ||
func (e Extractor) ToPURL(i *extractor.Inventory) *purl.PackageURL { | ||
return &purl.PackageURL{ | ||
Type: purl.TypeNPM, | ||
Name: strings.ToLower(i.Name), | ||
Version: i.Version, | ||
} | ||
} | ||
|
||
// Ecosystem returns the OSV ecosystem ('npm') of the software extracted by this extractor. | ||
func (e Extractor) Ecosystem(_ *extractor.Inventory) string { return "npm" } |
Oops, something went wrong.