Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ The detector supports parsing the following lockfiles:
| `gradle.lockfile` | `CRAN` | `renv` |
| `mix.lock` | `Hex` | `mix` |
| `poetry.lock` | `PyPI` | `poetry` |
| `uv.lock` | `PyPI` | `uv` |
| `Pipfile.lock` | `PyPI` | `pipenv` |
| `pdm.lock` | `PyPI` | `pdm` |
| `pubspec.lock` | `Pub` | `pub` |
Expand Down
1 change: 1 addition & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ func TestRun(t *testing.T) {
pubspec.lock
renv.lock
requirements.txt
uv.lock
yarn.lock
csv-file
csv-row
Expand Down
4 changes: 2 additions & 2 deletions pkg/lockfile/ecosystems_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@ func TestKnownEcosystems(t *testing.T) {
expectedCount := numberOfLockfileParsers(t)

// - npm, yarn, bun, and pnpm,
// - pip, poetry, pdm and pipenv,
// - pip, poetry, uv, pdm and pipenv,
// - maven and gradle,
// all use the same ecosystem so "ignore" those parsers in the count
expectedCount -= 7
expectedCount -= 8

ecosystems := lockfile.KnownEcosystems()

Expand Down
Empty file.
220 changes: 220 additions & 0 deletions pkg/lockfile/fixtures/uv/grouped-packages.lock

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions pkg/lockfile/fixtures/uv/no-dependencies.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
version = 1
requires-python = ">=3.10"

[[package]]
name = "uv-lockfiles"
version = "0.1.0"
source = { virtual = "." }
2 changes: 2 additions & 0 deletions pkg/lockfile/fixtures/uv/no-packages.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
version = 1
requires-python = ">=3.10"
1 change: 1 addition & 0 deletions pkg/lockfile/fixtures/uv/not-toml.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
this is not valid toml! (I think)
22 changes: 22 additions & 0 deletions pkg/lockfile/fixtures/uv/one-package.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
version = 1
requires-python = ">=3.10"

[[package]]
name = "emoji"
version = "2.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/64/812d7e2ae0ac2ade0d6583f911f99240c80f700afbe8391df10e547f564d/emoji-2.14.0.tar.gz", hash = "sha256:f68ac28915a2221667cddb3e6c589303c3c6954c6c5af6fefaec7f9bdf72fdca", size = 593932 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/56/4ddf8b36aa4b52077045b17ffb8958f3360b250df4143d1482d9d5bb54d5/emoji-2.14.0-py3-none-any.whl", hash = "sha256:fcc936bf374b1aec67dda5303ae99710ba88cc9cdce2d1a71c5f2204e6d78799", size = 586897 },
]

[[package]]
name = "uv-lockfiles"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "emoji" },
]

[package.metadata]
requires-dist = [{ name = "emoji" }]
18 changes: 18 additions & 0 deletions pkg/lockfile/fixtures/uv/source-git.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
version = 1
requires-python = ">=3.10"

[[package]]
name = "ruff"
version = "0.8.1"
source = { git = "https://github.com/astral-sh/ruff#84748be16341b76e073d117329f7f5f4ee2941ad" }

[[package]]
name = "uv-lockfiles"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "ruff" },
]

[package.metadata]
requires-dist = [{ name = "ruff", git = "https://github.com/astral-sh/ruff" }]
40 changes: 40 additions & 0 deletions pkg/lockfile/fixtures/uv/two-packages.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
version = 1
requires-python = ">=3.10"

[[package]]
name = "emoji"
version = "2.14.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/64/812d7e2ae0ac2ade0d6583f911f99240c80f700afbe8391df10e547f564d/emoji-2.14.0.tar.gz", hash = "sha256:f68ac28915a2221667cddb3e6c589303c3c6954c6c5af6fefaec7f9bdf72fdca", size = 593932 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/56/4ddf8b36aa4b52077045b17ffb8958f3360b250df4143d1482d9d5bb54d5/emoji-2.14.0-py3-none-any.whl", hash = "sha256:fcc936bf374b1aec67dda5303ae99710ba88cc9cdce2d1a71c5f2204e6d78799", size = 586897 },
]

[[package]]
name = "protobuf"
version = "4.25.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/67/dd/48d5fdb68ec74d70fabcc252e434492e56f70944d9f17b6a15e3746d2295/protobuf-4.25.5.tar.gz", hash = "sha256:7f8249476b4a9473645db7f8ab42b02fe1488cbe5fb72fddd445e0665afd8584", size = 380315 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/00/35/1b3c5a5e6107859c4ca902f4fbb762e48599b78129a05d20684fef4a4d04/protobuf-4.25.5-cp310-abi3-win32.whl", hash = "sha256:5e61fd921603f58d2f5acb2806a929b4675f8874ff5f330b7d6f7e2e784bbcd8", size = 392457 },
{ url = "https://files.pythonhosted.org/packages/a7/ad/bf3f358e90b7e70bf7fb520702cb15307ef268262292d3bdb16ad8ebc815/protobuf-4.25.5-cp310-abi3-win_amd64.whl", hash = "sha256:4be0571adcbe712b282a330c6e89eae24281344429ae95c6d85e79e84780f5ea", size = 413449 },
{ url = "https://files.pythonhosted.org/packages/51/49/d110f0a43beb365758a252203c43eaaad169fe7749da918869a8c991f726/protobuf-4.25.5-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:b2fde3d805354df675ea4c7c6338c1aecd254dfc9925e88c6d31a2bcb97eb173", size = 394248 },
{ url = "https://files.pythonhosted.org/packages/c6/ab/0f384ca0bc6054b1a7b6009000ab75d28a5506e4459378b81280ae7fd358/protobuf-4.25.5-cp37-abi3-manylinux2014_aarch64.whl", hash = "sha256:919ad92d9b0310070f8356c24b855c98df2b8bd207ebc1c0c6fcc9ab1e007f3d", size = 293717 },
{ url = "https://files.pythonhosted.org/packages/05/a6/094a2640be576d760baa34c902dcb8199d89bce9ed7dd7a6af74dcbbd62d/protobuf-4.25.5-cp37-abi3-manylinux2014_x86_64.whl", hash = "sha256:fe14e16c22be926d3abfcb500e60cab068baf10b542b8c858fa27e098123e331", size = 294635 },
{ url = "https://files.pythonhosted.org/packages/33/90/f198a61df8381fb43ae0fe81b3d2718e8dcc51ae8502c7657ab9381fbc4f/protobuf-4.25.5-py3-none-any.whl", hash = "sha256:0aebecb809cae990f8129ada5ca273d9d670b76d9bfc9b1809f0a9c02b7dbf41", size = 156467 },
]

[[package]]
name = "uv-lockfiles"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "emoji" },
{ name = "protobuf" },
]

[package.metadata]
requires-dist = [
{ name = "emoji" },
{ name = "protobuf", specifier = ">=3.19.0,<5.0.0.dev0" },
]
72 changes: 72 additions & 0 deletions pkg/lockfile/parse-uv-lock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package lockfile

import (
"fmt"
"os"
"strings"

"github.com/BurntSushi/toml"
)

type UvLockPackageSource struct {
Virtual string `toml:"virtual"`
Git string `toml:"git"`
}

type UvLockPackage struct {
Name string `toml:"name"`
Version string `toml:"version"`
Source UvLockPackageSource `toml:"source"`

// uv stores "groups" as a table under "package" after all the packages, which due
// to how TOML works means it ends up being a property on the last package, even
// through in this context it's a global property rather than being per-package
Groups map[string][]UvOptionalDependency `toml:"optional-dependencies"`
}

type UvOptionalDependency struct {
Name string `toml:"name"`
}
type UvLockFile struct {
Version int `toml:"version"`
Packages []UvLockPackage `toml:"package"`
}

const UvEcosystem = PipEcosystem

func ParseUvLock(pathToLockfile string) ([]PackageDetails, error) {
var parsedLockfile *UvLockFile

lockfileContents, err := os.ReadFile(pathToLockfile)

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

err = toml.Unmarshal(lockfileContents, &parsedLockfile)

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

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

for _, lockPackage := range parsedLockfile.Packages {
// skip including the root "package", since its name and version are most likely arbitrary
if lockPackage.Source.Virtual == "." {
continue
}

_, commit, _ := strings.Cut(lockPackage.Source.Git, "#")

packages = append(packages, PackageDetails{
Name: lockPackage.Name,
Version: lockPackage.Version,
Ecosystem: UvEcosystem,
CompareAs: UvEcosystem,
Commit: commit,
})
}

return packages, nil
}
198 changes: 198 additions & 0 deletions pkg/lockfile/parse-uv-lock_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package lockfile_test

import (
"testing"

"github.com/g-rath/osv-detector/pkg/lockfile"
)

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

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

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

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

packages, err := lockfile.ParseUvLock("fixtures/uv/not-toml.txt")

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

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

packages, err := lockfile.ParseUvLock("fixtures/uv/empty.lock")

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

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

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

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

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

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "emoji",
Version: "2.14.0",
Ecosystem: lockfile.UvEcosystem,
CompareAs: lockfile.UvEcosystem,
},
})
}

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

packages, err := lockfile.ParseUvLock("fixtures/uv/two-packages.lock")

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

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "emoji",
Version: "2.14.0",
Ecosystem: lockfile.UvEcosystem,
CompareAs: lockfile.UvEcosystem,
},
{
Name: "protobuf",
Version: "4.25.5",
Ecosystem: lockfile.UvEcosystem,
CompareAs: lockfile.UvEcosystem,
},
})
}

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

packages, err := lockfile.ParseUvLock("fixtures/uv/source-git.lock")

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

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "ruff",
Version: "0.8.1",
Ecosystem: lockfile.UvEcosystem,
CompareAs: lockfile.UvEcosystem,
Commit: "84748be16341b76e073d117329f7f5f4ee2941ad",
},
})
}

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

packages, err := lockfile.ParseUvLock("fixtures/uv/grouped-packages.lock")

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

expectPackages(t, packages, []lockfile.PackageDetails{
{
Name: "emoji",
Version: "2.14.0",
Ecosystem: lockfile.UvEcosystem,
CompareAs: lockfile.UvEcosystem,
},
{
Name: "click",
Version: "8.1.7",
Ecosystem: lockfile.UvEcosystem,
CompareAs: lockfile.UvEcosystem,
},
{
Name: "colorama",
Version: "0.4.6",
Ecosystem: lockfile.UvEcosystem,
CompareAs: lockfile.UvEcosystem,
},
{
Name: "black",
Version: "24.10.0",
Ecosystem: lockfile.UvEcosystem,
CompareAs: lockfile.UvEcosystem,
},
{
Name: "flake8",
Version: "7.1.1",
Ecosystem: lockfile.UvEcosystem,
CompareAs: lockfile.UvEcosystem,
},
{
Name: "mccabe",
Version: "0.7.0",
Ecosystem: lockfile.UvEcosystem,
CompareAs: lockfile.UvEcosystem,
},
{
Name: "mypy-extensions",
Version: "1.0.0",
Ecosystem: lockfile.UvEcosystem,
CompareAs: lockfile.UvEcosystem,
},
{
Name: "packaging",
Version: "24.2",
Ecosystem: lockfile.UvEcosystem,
CompareAs: lockfile.UvEcosystem,
},
{
Name: "pathspec",
Version: "0.12.1",
Ecosystem: lockfile.UvEcosystem,
CompareAs: lockfile.UvEcosystem,
},
{
Name: "platformdirs",
Version: "4.3.6",
Ecosystem: lockfile.UvEcosystem,
CompareAs: lockfile.UvEcosystem,
},
{
Name: "pycodestyle",
Version: "2.12.1",
Ecosystem: lockfile.UvEcosystem,
CompareAs: lockfile.UvEcosystem,
},
{
Name: "pyflakes",
Version: "3.2.0",
Ecosystem: lockfile.UvEcosystem,
CompareAs: lockfile.UvEcosystem,
},
{
Name: "tomli",
Version: "2.2.1",
Ecosystem: lockfile.UvEcosystem,
CompareAs: lockfile.UvEcosystem,
},
{
Name: "typing-extensions",
Version: "4.12.2",
Ecosystem: lockfile.UvEcosystem,
CompareAs: lockfile.UvEcosystem,
},
})
}
Loading