Skip to content

Commit

Permalink
feat: add support for renv.lock (#668)
Browse files Browse the repository at this point in the history
Resolves #642
  • Loading branch information
G-Rath authored Nov 27, 2023
1 parent aa3ca89 commit e99410e
Show file tree
Hide file tree
Showing 14 changed files with 331 additions and 1 deletion.
3 changes: 2 additions & 1 deletion docs/supported_languages_and_lockfiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ A wide range of lockfiles are supported by utilizing this [lockfile package](htt
| Javascript | `package-lock.json`<br>`pnpm-lock.yaml`<br>`yarn.lock`|
| PHP | `composer.lock`|
| Python | `Pipfile.lock`<br>`poetry.lock`<br>`requirements.txt`[\*](https://github.com/google/osv-scanner/issues/34) |
| R | `renv.lock` |
| Ruby | `Gemfile.lock`|
| Rust | `Cargo.lock`|

Expand Down Expand Up @@ -102,4 +103,4 @@ Once you extracted your own dependency information, place it in a `osv-scanner.j
Then pass this to `osv-scanner` with this:
```
osv-scanner --lockfile osv-scanner:/path/to/osv-scanner.json
```
```
1 change: 1 addition & 0 deletions pkg/lockfile/ecosystems.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func KnownEcosystems() []Ecosystem {
PipEcosystem,
PubEcosystem,
ConanEcosystem,
CRANEcosystem,
// Disabled temporarily,
// see https://github.com/google/osv-scanner/pull/128 discussion for additional context
// AlpineEcosystem,
Expand Down
2 changes: 2 additions & 0 deletions pkg/lockfile/extract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func TestFindExtractor(t *testing.T) {
"poetry.lock": "poetry.lock",
"pom.xml": "pom.xml",
"pubspec.lock": "pubspec.lock",
"renv.lock": "renv.lock",
"requirements.txt": "requirements.txt",
"yarn.lock": "yarn.lock",
}
Expand Down Expand Up @@ -98,6 +99,7 @@ func TestExtractDeps_FindsExpectedExtractor(t *testing.T) {
"poetry.lock",
"pom.xml",
"pubspec.lock",
"renv.lock",
"requirements.txt",
"yarn.lock",
}
Expand Down
1 change: 1 addition & 0 deletions pkg/lockfile/fixtures/renv/empty.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
1 change: 1 addition & 0 deletions pkg/lockfile/fixtures/renv/not-json.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
this is not json!
17 changes: 17 additions & 0 deletions pkg/lockfile/fixtures/renv/one-package.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"R": {
"Version": "2.15.2",
"Repositories": []
},
"Packages": {
"morning": {
"Package": "morning",
"Version": "0.1.0",
"Repository": "CRAN",
"Requirements": [
"coffee",
"toast"
]
}
}
}
27 changes: 27 additions & 0 deletions pkg/lockfile/fixtures/renv/two-packages.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"R": {
"Version": "4.2.3",
"Repositories": [
{
"Name": "CRAN",
"URL": "https://cloud.r-project.org"
}
]
},
"Packages": {
"markdown": {
"Package": "markdown",
"Version": "1.0",
"Source": "Repository",
"Repository": "CRAN",
"Hash": "4584a57f565dd7987d59dda3a02cfb41"
},
"mime": {
"Package": "mime",
"Version": "0.7",
"Source": "Repository",
"Repository": "CRAN",
"Hash": "908d95ccbfd1dd274073ef07a7c93934"
}
}
}
29 changes: 29 additions & 0 deletions pkg/lockfile/fixtures/renv/with-bioconductor.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"R": {
"Version": "4.1.0",
"Repositories": [
{
"Name": "CRAN",
"URL": "https://cran.rstudio.com"
}
]
},
"Bioconductor": {
"Version": "3.13"
},
"Packages": {
"BH": {
"Package": "BH",
"Version": "1.75.0-0",
"Source": "Repository",
"Repository": "CRAN",
"Hash": "e4c04affc2cac20c8fec18385cd14691"
},
"BSgenome": {
"Package": "BSgenome",
"Version": "1.60.0",
"Source": "Bioconductor",
"Hash": "bc39f66b170caed3ea67c03eb6b4b55c"
}
}
}
35 changes: 35 additions & 0 deletions pkg/lockfile/fixtures/renv/with-mixed-sources.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"R": {
"Version": "4.3.1",
"Repositories": [
{
"Name": "CRAN",
"URL": "https://cloud.r-project.org"
}
]
},
"Packages": {
"markdown": {
"Package": "markdown",
"Version": "1.0",
"Source": "Repository",
"Repository": "CRAN",
"Hash": "4584a57f565dd7987d59dda3a02cfb41"
},
"mime": {
"Package": "mime",
"Version": "0.12.1",
"Source": "GitHub",
"RemoteType": "github",
"RemoteHost": "api.github.com",
"RemoteUsername": "yihui",
"RemoteRepo": "mime",
"RemoteRef": "main",
"RemoteSha": "1763e0dcb72fb58d97bab97bb834fc71f1e012bc",
"Requirements": [
"tools"
],
"Hash": "c2772b6269924dad6784aaa1d99dbb86"
}
}
}
16 changes: 16 additions & 0 deletions pkg/lockfile/fixtures/renv/without-repository.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"R": {
"Version": "2.15.2",
"Repositories": []
},
"Packages": {
"morning": {
"Package": "morning",
"Version": "0.1.0",
"Requirements": [
"coffee",
"toast"
]
}
}
}
64 changes: 64 additions & 0 deletions pkg/lockfile/parse-renv-lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package lockfile

import (
"encoding/json"
"fmt"
"path/filepath"
)

type RenvPackage struct {
Package string `json:"Package"`
Version string `json:"Version"`
Repository string `json:"Repository"`
}

type RenvLockfile struct {
Packages map[string]RenvPackage `json:"Packages"`
}

const CRANEcosystem Ecosystem = "CRAN"

type RenvLockExtractor struct{}

func (e RenvLockExtractor) ShouldExtract(path string) bool {
return filepath.Base(path) == "renv.lock"
}

func (e RenvLockExtractor) Extract(f DepFile) ([]PackageDetails, error) {
var parsedLockfile *RenvLockfile

err := json.NewDecoder(f).Decode(&parsedLockfile)

if err != nil {
return []PackageDetails{}, fmt.Errorf("could not extract from %s: %w", f.Path(), err)
}

packages := make([]PackageDetails, 0, len(parsedLockfile.Packages))

for _, pkg := range parsedLockfile.Packages {
// currently we only support CRAN
if pkg.Repository != string(CRANEcosystem) {
continue
}

packages = append(packages, PackageDetails{
Name: pkg.Package,
Version: pkg.Version,
Ecosystem: CRANEcosystem,
CompareAs: CRANEcosystem,
})
}

return packages, nil
}

var _ Extractor = RenvLockExtractor{}

//nolint:gochecknoinits
func init() {
registerExtractor("renv.lock", RenvLockExtractor{})
}

func ParseRenvLock(pathToLockfile string) ([]PackageDetails, error) {
return extractFromFile(pathToLockfile, RenvLockExtractor{})
}
133 changes: 133 additions & 0 deletions pkg/lockfile/parse-renv-lock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package lockfile_test

import (
"io/fs"
"testing"

"github.com/google/osv-scanner/pkg/lockfile"
)

func TestParseRenvLock_FileDoesNotExist(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseRenvLock("fixtures/renv/does-not-exist")

expectErrIs(t, err, fs.ErrNotExist)
expectPackages(t, packages, []lockfile.PackageDetails{})
}

func TestParseRenvLock_InvalidJson(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseRenvLock("fixtures/renv/not-json.txt")

expectErrContaining(t, err, "could not extract from")
expectPackages(t, packages, []lockfile.PackageDetails{})
}

func TestParseRenvLock_NoPackages(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseRenvLock("fixtures/renv/empty.lock")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{})
}

func TestParseRenvLock_OnePackage(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseRenvLock("fixtures/renv/one-package.lock")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "morning",
Version: "0.1.0",
Ecosystem: lockfile.CRANEcosystem,
CompareAs: lockfile.CRANEcosystem,
},
})
}

func TestParseRenvLock_TwoPackages(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseRenvLock("fixtures/renv/two-packages.lock")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "markdown",
Version: "1.0",
Ecosystem: lockfile.CRANEcosystem,
CompareAs: lockfile.CRANEcosystem,
},
{
Name: "mime",
Version: "0.7",
Ecosystem: lockfile.CRANEcosystem,
CompareAs: lockfile.CRANEcosystem,
},
})
}

func TestParseRenvLock_WithMixedSources(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseRenvLock("fixtures/renv/with-mixed-sources.lock")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "markdown",
Version: "1.0",
Ecosystem: lockfile.CRANEcosystem,
CompareAs: lockfile.CRANEcosystem,
},
})
}

func TestParseRenvLock_WithBioconductor(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseRenvLock("fixtures/renv/with-bioconductor.lock")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

// currently Bioconductor is not supported
expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "BH",
Version: "1.75.0-0",
Ecosystem: lockfile.CRANEcosystem,
CompareAs: lockfile.CRANEcosystem,
},
})
}

func TestParseRenvLock_WithoutRepository(t *testing.T) {
t.Parallel()

packages, err := lockfile.ParseRenvLock("fixtures/renv/without-repository.lock")

if err != nil {
t.Errorf("Got unexpected error: %v", err)
}

expectPackages(t, packages, []lockfile.PackageDetails{})
}
1 change: 1 addition & 0 deletions pkg/lockfile/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ var parsers = map[string]PackageDetailsParser{
"poetry.lock": ParsePoetryLock,
"pom.xml": ParseMavenLock,
"pubspec.lock": ParsePubspecLock,
"renv.lock": ParseRenvLock,
"requirements.txt": ParseRequirementsTxt,
"yarn.lock": ParseYarnLock,
}
Expand Down
2 changes: 2 additions & 0 deletions pkg/lockfile/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ func TestFindParser(t *testing.T) {
"poetry.lock",
"pom.xml",
"pubspec.lock",
"renv.lock",
"requirements.txt",
"yarn.lock",
}
Expand Down Expand Up @@ -107,6 +108,7 @@ func TestParse_FindsExpectedParsers(t *testing.T) {
"poetry.lock",
"pom.xml",
"pubspec.lock",
"renv.lock",
"requirements.txt",
"yarn.lock",
}
Expand Down

0 comments on commit e99410e

Please sign in to comment.