Skip to content

Commit

Permalink
Add purl properties to all yarn classic packages
Browse files Browse the repository at this point in the history
Each "yarn classic" package type has a property that returns the package
URL based on its attributes and community PURL specification [1].

All package types share the same base -> name, version, and type which
is set to "npm" ("yarn" does not exist).

- `FilePackage`, `WorkspacePackage`, `LinkPackage` have in addition
  subpath component (extra subpath within a package, relative to the
  package root)

- `UrlPackage` has one extra qualifier -> its URL as it is definied

- `GitPackage` has one extra qualifier -> package version control system
  URL with a specific syntax [2]

- `RegistryPackage` has two extra qualifiers -> repository_url (default
  repository/registry for "npm" is https://registry.npmjs.org so
  alternative registries such as https://registry.yarnpkg.com should be
  qualified via the qualifier) [3], [4] + the checksum of the package
  converted from Subresource Integrity representation

---
[1]: https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst
[2]: https://github.com/spdx/spdx-spec/blob/cfa1b9d08903/chapters/3-package-information.md#37-package-download-location-
[3]: https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#npm
[4]: https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst#known-qualifiers-keyvalue-pairs

Signed-off-by: Michal Šoltis <[email protected]>
  • Loading branch information
slimreaper35 committed Nov 19, 2024
1 parent ba3f23b commit ef8f4c2
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 1 deletion.
79 changes: 79 additions & 0 deletions cachi2/core/package_managers/yarn_classic/resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
from typing import Iterable, Optional, Union
from urllib.parse import urlparse

from packageurl import PackageURL
from pyarn.lockfile import Package as PYarnPackage
from pydantic import BaseModel

from cachi2.core.checksum import ChecksumInfo
from cachi2.core.errors import PackageRejected, UnexpectedFormat
from cachi2.core.package_managers.npm import NPM_REGISTRY_CNAMES
from cachi2.core.package_managers.yarn_classic.project import PackageJson, Project, YarnLock
Expand All @@ -15,6 +17,7 @@
extract_workspace_metadata,
)
from cachi2.core.rooted_path import RootedPath
from cachi2.core.scm import RepoID

# https://github.com/yarnpkg/yarn/blob/7cafa512a777048ce0b666080a24e80aae3d66a9/src/resolvers/exotics/git-resolver.js#L15-L17
GIT_HOSTS = frozenset(("github.com", "gitlab.com", "bitbucket.com", "bitbucket.org"))
Expand All @@ -26,6 +29,9 @@
re.compile(r"^https?:.+\.git#.+"),
)

DEFAULT_NPM_REGISTRY = "https://registry.npmjs.org"
ALTERNATIVE_NPM_REGISTRY = "https://registry.yarnpkg.com"


class _BasePackage(BaseModel):
"""A base Yarn 1.x package."""
Expand All @@ -47,26 +53,99 @@ class _RelpathMixin(BaseModel):
class RegistryPackage(_BasePackage, _UrlMixin):
"""A Yarn 1.x package from the registry."""

@property
def purl(self) -> str:
"""Return package URL."""
qualifiers = {}

if self.url != DEFAULT_NPM_REGISTRY:
qualifiers = {"repository_url": ALTERNATIVE_NPM_REGISTRY}

if self.integrity:
checksum = ChecksumInfo.from_sri(self.integrity)
qualifiers["checksum"] = str(checksum)

return PackageURL(
type="npm",
name=self.name,
version=self.version,
qualifiers=qualifiers,
).to_string()


class GitPackage(_BasePackage, _UrlMixin):
"""A Yarn 1.x package from a git repo."""

@property
def purl(self) -> str:
"""Return package URL."""
parsed_url = urlparse(self.url)
repo_id = RepoID(origin_url=self.url, commit_id=parsed_url.fragment)
qualifiers = {"vcs_url": repo_id.as_vcs_url_qualifier()}
return PackageURL(
type="npm",
name=self.name,
version=self.version,
qualifiers=qualifiers,
).to_string()


class UrlPackage(_BasePackage, _UrlMixin):
"""A Yarn 1.x package from a http/https URL."""

@property
def purl(self) -> str:
"""Return package URL."""
qualifiers = {"download_url": self.url}
return PackageURL(
type="npm",
name=self.name,
version=self.version,
qualifiers=qualifiers,
).to_string()


class FilePackage(_BasePackage, _RelpathMixin):
"""A Yarn 1.x package from a local file path."""

@property
def purl(self) -> str:
"""Return package URL."""
return PackageURL(
type="npm",
name=self.name,
version=self.version,
subpath=str(self.relpath),
).to_string()


class WorkspacePackage(_BasePackage, _RelpathMixin):
"""A Yarn 1.x local workspace package."""

@property
def purl(self) -> str:
"""Return package URL."""
return PackageURL(
type="npm",
name=self.name,
version=self.version,
subpath=str(self.relpath),
).to_string()


class LinkPackage(_BasePackage, _RelpathMixin):
"""A Yarn 1.x local link package."""

@property
def purl(self) -> str:
"""Return package URL."""
return PackageURL(
type="npm",
name=self.name,
version=self.version,
subpath=str(self.relpath),
).to_string()


YarnClassicPackage = Union[
FilePackage,
Expand Down
79 changes: 78 additions & 1 deletion tests/unit/package_managers/yarn_classic/test_resolver.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import re
from pathlib import Path
from unittest import mock
from urllib.parse import quote

import pytest
from pyarn.lockfile import Package as PYarnPackage

from cachi2.core.checksum import ChecksumInfo
from cachi2.core.errors import PackageRejected, UnexpectedFormat
from cachi2.core.package_managers.yarn_classic.project import PackageJson
from cachi2.core.package_managers.yarn_classic.resolver import (
Expand All @@ -26,6 +28,8 @@
)
from cachi2.core.package_managers.yarn_classic.workspaces import Workspace
from cachi2.core.rooted_path import PathOutsideRoot, RootedPath
from cachi2.core.scm import RepoID
from tests.common_utils import GIT_REF

VALID_GIT_URLS = [
"git://git.host.com/some/path",
Expand Down Expand Up @@ -233,7 +237,6 @@ def test_create_package_from_pyarn_package_fail_unexpected_format(
def test__get_packages_from_lockfile(
mock_create_package: mock.Mock, rooted_tmp_path: RootedPath
) -> None:

# Setup lockfile instance
mock_pyarn_lockfile = mock.Mock()
mock_yarn_lock = mock.Mock(yarn_lockfile=mock_pyarn_lockfile)
Expand Down Expand Up @@ -339,3 +342,77 @@ def test__get_workspace_packages(rooted_tmp_path: RootedPath) -> None:

output = _get_workspace_packages(rooted_tmp_path, [workspace])
assert output == expected


def test_package_purl() -> None:
example_git_url = f"https://github.com/org/repo.git#{GIT_REF}"
example_repo_id = RepoID(origin_url=example_git_url, commit_id=GIT_REF)
example_vcs_url = example_repo_id.as_vcs_url_qualifier()
purl_vcs_url = quote(example_vcs_url, safe=":/")

example_sri_integrity = "sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA=="
example_checksum = ChecksumInfo.from_sri(example_sri_integrity)

yarn_classic_packages: list[tuple[YarnClassicPackage, str]] = [
(
RegistryPackage(
name="registry-pkg",
version="1.0.0",
integrity=example_sri_integrity,
url="https://registry.npmjs.org",
),
f"pkg:npm/[email protected]?checksum={str(example_checksum)}",
),
(
RegistryPackage(
name="registry-pkg-alternative",
version="2.0.0",
integrity=example_sri_integrity,
url="https://registry.yarnpkg.com",
),
f"pkg:npm/[email protected]?checksum={str(example_checksum)}&repository_url=https://registry.yarnpkg.com",
),
(
GitPackage(
name="git-pkg",
version="3.0.0",
url=example_git_url,
),
f"pkg:npm/[email protected]?vcs_url={purl_vcs_url}",
),
(
UrlPackage(
name="url-pkg",
version="4.0.0",
url="https://example.com/package.tar.gz",
),
"pkg:npm/[email protected]?download_url=https://example.com/package.tar.gz",
),
(
FilePackage(
name="file-pkg",
version="5.0.0",
relpath=Path("path/to/package"),
),
"pkg:npm/[email protected]#path/to/package",
),
(
WorkspacePackage(
name="workspace-pkg",
version="6.0.0",
relpath=Path("workspace/package"),
),
"pkg:npm/[email protected]#workspace/package",
),
(
LinkPackage(
name="link-pkg",
version="7.0.0",
relpath=Path("link/to/package"),
),
"pkg:npm/[email protected]#link/to/package",
),
]

for package, expected_purl in yarn_classic_packages:
assert package.purl == expected_purl

0 comments on commit ef8f4c2

Please sign in to comment.