Skip to content

feat(GH-188): Implement package type providers #223

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
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
16 changes: 13 additions & 3 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@

<developers>
<developer>
<id>steve.springett</id>
<name>Steve Springett</name>
<email>Steve.Springett@owasp.org</email>
<organization>OWASP</organization>
<organizationUrl>http://www.owasp.org/</organizationUrl>
<organizationUrl>https://www.owasp.org/</organizationUrl>
<roles>
<role>Architect</role>
<role>Developer</role>
Expand Down Expand Up @@ -142,6 +143,9 @@
<json.version>20250107</json.version>
<junit-bom.version>5.12.1</junit-bom.version>
<maven-surefire-junit5-tree-reporter.version>1.4.0</maven-surefire-junit5-tree-reporter.version>
<biz.aQute.bnd.annotation.version>7.1.0</biz.aQute.bnd.annotation.version>
<jspecify.version>1.0.0</jspecify.version>
<org.osgi.annotation.bundle.version>2.0.0</org.osgi.annotation.bundle.version>
</properties>

<dependencyManagement>
Expand All @@ -158,6 +162,12 @@
</dependencyManagement>

<dependencies>
<dependency>
<groupId>biz.aQute.bnd</groupId>
<artifactId>biz.aQute.bnd.annotation</artifactId>
<version>${biz.aQute.bnd.annotation.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
Expand All @@ -168,13 +178,13 @@
<dependency>
<groupId>org.osgi</groupId>
<artifactId>org.osgi.annotation.bundle</artifactId>
<version>2.0.0</version>
<version>${org.osgi.annotation.bundle.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jspecify</groupId>
<artifactId>jspecify</artifactId>
<version>1.0.0</version>
<version>${jspecify.version}</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
Expand Down
60 changes: 33 additions & 27 deletions src/main/java/com/github/packageurl/PackageURL.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

import static java.util.Objects.requireNonNull;

import com.github.packageurl.internal.PackageTypeFactory;
import com.github.packageurl.internal.StringUtil;
import java.io.Serializable;
import java.net.URI;
Expand Down Expand Up @@ -73,34 +74,34 @@ public final class PackageURL implements Serializable {
private final String type;

/**
* The name prefix such as a Maven groupid, a Docker image owner, a GitHub user or organization.
* The name prefix such as a Maven groupId, a Docker image owner, a GitHub user or organization.
* Optional and type-specific.
*/
private final @Nullable String namespace;
private @Nullable String namespace;

/**
* The name of the package.
* Required.
*/
private final String name;
private String name;

/**
* The version of the package.
* Optional.
*/
private final @Nullable String version;
private @Nullable String version;

/**
* Extra qualifying data for a package such as an OS, architecture, a distro, etc.
* Optional and type-specific.
*/
private final @Nullable Map<String, String> qualifiers;
private @Nullable Map<String, String> qualifiers;

/**
* Extra subpath within a package, relative to the package root.
* Optional.
*/
private final @Nullable String subpath;
private @Nullable String subpath;

/**
* Constructs a new PackageURL object by parsing the specified string.
Expand Down Expand Up @@ -190,7 +191,6 @@ public PackageURL(final String purl) throws MalformedPackageURLException {
remainder = remainder.substring(0, index);
this.namespace = validateNamespace(this.type, parsePath(remainder.substring(start), false));
}
verifyTypeConstraints(this.type, this.namespace, this.name);
} catch (URISyntaxException e) {
throw new MalformedPackageURLException("Invalid purl: " + e.getMessage(), e);
}
Expand Down Expand Up @@ -235,7 +235,6 @@ public PackageURL(
this.version = validateVersion(this.type, version);
this.qualifiers = parseQualifiers(qualifiers);
this.subpath = validateSubpath(subpath);
verifyTypeConstraints(this.type, this.namespace, this.name);
}

/**
Expand Down Expand Up @@ -501,6 +500,18 @@ private static void validateValue(final String key, final @Nullable String value
}
}

/**
* Returns a new Package URL which is normalized.
*
* @return the normalized package URL
* @throws MalformedPackageURLException if an error occurs while normalizing this package URL
*/
public PackageURL normalize() throws MalformedPackageURLException {
PackageTypeFactory.getInstance().validateComponents(type, namespace, name, version, qualifiers, subpath);
return PackageTypeFactory.getInstance()
.normalizeComponents(type, namespace, name, version, qualifiers, subpath);
}

/**
* Returns the canonicalized representation of the purl.
*
Expand Down Expand Up @@ -528,6 +539,17 @@ public String canonicalize() {
* @since 1.3.2
*/
private String canonicalize(boolean coordinatesOnly) {
try {
PackageURL packageURL = normalize();
namespace = packageURL.getNamespace();
name = packageURL.getName();
version = packageURL.getVersion();
qualifiers = packageURL.getQualifiers();
subpath = packageURL.getSubpath();
} catch (MalformedPackageURLException e) {
throw new ValidationException("Normalization failed", e);
}

Comment on lines +542 to +552
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is kind of a hack right now.

final StringBuilder purl = new StringBuilder();
purl.append(SCHEME_PART).append(type).append('/');
if (namespace != null) {
Expand All @@ -540,7 +562,7 @@ private String canonicalize(boolean coordinatesOnly) {
}

if (!coordinatesOnly) {
if (qualifiers != null) {
if (!qualifiers.isEmpty()) {
purl.append('?');
Set<Map.Entry<String, String>> entries = qualifiers.entrySet();
boolean separator = false;
Expand All @@ -561,22 +583,6 @@ private String canonicalize(boolean coordinatesOnly) {
return purl.toString();
}

/**
* Some purl types may have specific constraints. This method attempts to verify them.
* @param type the purl type
* @param namespace the purl namespace
* @throws MalformedPackageURLException if constraints are not met
*/
private static void verifyTypeConstraints(String type, @Nullable String namespace, @Nullable String name)
throws MalformedPackageURLException {
if (StandardTypes.MAVEN.equals(type)) {
if (isEmpty(namespace) || isEmpty(name)) {
throw new MalformedPackageURLException(
"The PackageURL specified is invalid. Maven requires both a namespace and name.");
}
}
}

private static @Nullable Map<String, String> parseQualifiers(final @Nullable Map<String, String> qualifiers)
throws MalformedPackageURLException {
if (qualifiers == null || qualifiers.isEmpty()) {
Expand Down Expand Up @@ -898,15 +904,15 @@ public static final class StandardTypes {
* @deprecated use {@link #DEB} instead
*/
@Deprecated
public static final String DEBIAN = "deb";
public static final String DEBIAN = DEB;
/**
* Nixos packages.
*
* @since 1.1.0
* @deprecated use {@link #NIX} instead
*/
@Deprecated
public static final String NIXPKGS = "nix";
public static final String NIXPKGS = NIX;

private StandardTypes() {}
}
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/com/github/packageurl/ValidationException.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,8 @@ public class ValidationException extends RuntimeException {
public ValidationException(String msg) {
super(msg);
}

ValidationException(String msg, Throwable cause) {
super(msg, cause);
}
}
Loading