Skip to content

Commit af34fc4

Browse files
authored
fix: URI creation (#172)
* Fix URI creation Remove the `scheme` field and replace it with a `SCHEME` constant as its impossible to create a `PackageURL` with a different `scheme`. This should not change the public API. It was previously the case that parsing a purl could produce different `URI` representations internally based on the number of slashes present after the scheme colon in the input. For example, a purl with the canonical form `pkg:generic/name` when input starting with `pkg:`, `pkg:/`, and `pkg://` will produce three different `URI` representations. By normalizing the input to contain a single `'/'`, as in `pkg:/generic/name`, we obtain a valid hierarchical `URI` with all five standard components `[scheme:][//authority][path][?query][#fragment]` properly parsed. For the `URI` to be a valid purl, `scheme:` must be `"pkg:"`, `authority` must be `null`, and `?query` and `#fragment` are optional. Then, we only have to parse the `path` containing the `type/namespace/name@version` portion of the purl. * Use constant to determine start of parse * Make `SCHEME_PART` private * Use `SCHEME_PART` when canonicalizing
1 parent d23a0ee commit af34fc4

File tree

1 file changed

+53
-53
lines changed

1 file changed

+53
-53
lines changed

src/main/java/com/github/packageurl/PackageURL.java

Lines changed: 53 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,6 @@ public PackageURL(final String type, final String name) throws MalformedPackageU
9494
public PackageURL(final String type, final String namespace, final String name, final String version,
9595
final TreeMap<String, String> qualifiers, final String subpath)
9696
throws MalformedPackageURLException {
97-
98-
this.scheme = validateScheme("pkg");
9997
this.type = validateType(type);
10098
this.namespace = validateNamespace(namespace);
10199
this.name = validateName(name);
@@ -106,9 +104,14 @@ public PackageURL(final String type, final String namespace, final String name,
106104
}
107105

108106
/**
109-
* The PackageURL scheme constant
107+
* The PackageURL scheme constant.
108+
*/
109+
public static final String SCHEME = "pkg";
110+
111+
/**
112+
* The PackageURL scheme ({@code "pkg"}) constant followed by a colon ({@code ':'}).
110113
*/
111-
private String scheme;
114+
private static final String SCHEME_PART = SCHEME + ':';
112115

113116
/**
114117
* The package "type" or package "protocol" such as maven, npm, nuget, gem, pypi, etc.
@@ -170,7 +173,7 @@ public PackageURLBuilder toBuilder() {
170173
* @since 1.0.0
171174
*/
172175
public String getScheme() {
173-
return scheme;
176+
return SCHEME;
174177
}
175178

176179
/**
@@ -233,11 +236,10 @@ public String getSubpath() {
233236
return subpath;
234237
}
235238

236-
private String validateScheme(final String value) throws MalformedPackageURLException {
237-
if ("pkg".equals(value)) {
238-
return "pkg";
239-
}
240-
throw new MalformedPackageURLException("The PackageURL scheme is invalid");
239+
private void validateScheme(final String value) throws MalformedPackageURLException {
240+
if (!SCHEME.equals(value)) {
241+
throw new MalformedPackageURLException("The PackageURL scheme '" + value + "' is invalid. It should be '" + SCHEME + "'");
242+
}
241243
}
242244

243245
private String validateType(final String value) throws MalformedPackageURLException {
@@ -397,7 +399,7 @@ public String canonicalize() {
397399
*/
398400
private String canonicalize(boolean coordinatesOnly) {
399401
final StringBuilder purl = new StringBuilder();
400-
purl.append(scheme).append(":");
402+
purl.append(SCHEME_PART);
401403
if (type != null) {
402404
purl.append(type);
403405
}
@@ -519,73 +521,73 @@ public static String uriDecode(String source) {
519521
*/
520522
private void parse(final String purl) throws MalformedPackageURLException {
521523
if (purl == null || purl.trim().isEmpty()) {
522-
throw new MalformedPackageURLException("Invalid purl: Contains an empty or null value");
524+
throw new MalformedPackageURLException("Invalid purl: Is empty or null");
523525
}
524526

525527
try {
526-
final URI uri = new URI(purl);
527-
// Check to ensure that none of these parts are parsed. If so, it's an invalid purl.
528-
if (uri.getUserInfo() != null || uri.getPort() != -1) {
529-
throw new MalformedPackageURLException("Invalid purl: Contains parts not supported by the purl spec");
528+
if (!purl.startsWith(SCHEME_PART)) {
529+
throw new MalformedPackageURLException("Invalid purl: " + purl + ". It does not start with '" + SCHEME_PART + "'");
530530
}
531531

532-
this.scheme = validateScheme(uri.getScheme());
532+
final int length = purl.length();
533+
int start = SCHEME_PART.length();
533534

534-
// subpath is optional - check for existence
535-
if (uri.getRawFragment() != null && !uri.getRawFragment().isEmpty()) {
536-
this.subpath = validatePath(parsePath(uri.getRawFragment(), true), true);
535+
while (start < length && '/' == purl.charAt(start)) {
536+
start++;
537537
}
538-
// This is the purl (minus the scheme) that needs parsed.
539-
final StringBuilder remainder = new StringBuilder(uri.getRawSchemeSpecificPart());
540538

541-
// qualifiers are optional - check for existence
542-
int index = remainder.lastIndexOf("?");
543-
if (index >= 0) {
544-
this.qualifiers = parseQualifiers(remainder.substring(index + 1));
545-
remainder.setLength(index);
539+
final URI uri = new URI(String.join("/", SCHEME_PART, purl.substring(start)));
540+
541+
validateScheme(uri.getScheme());
542+
543+
// Check to ensure that none of these parts are parsed. If so, it's an invalid purl.
544+
if (uri.getRawAuthority() != null) {
545+
throw new MalformedPackageURLException("Invalid purl: A purl must NOT contain a URL Authority ");
546+
}
547+
548+
// subpath is optional - check for existence
549+
final String rawFragment = uri.getRawFragment();
550+
if (rawFragment != null && !rawFragment.isEmpty()) {
551+
this.subpath = validatePath(parsePath(rawFragment, true), true);
546552
}
553+
// qualifiers are optional - check for existence
554+
final String rawQuery = uri.getRawQuery();
555+
if (rawQuery != null && !rawQuery.isEmpty()) {
556+
this.qualifiers = parseQualifiers(rawQuery);
547557

548-
// trim leading and trailing '/'
558+
}
559+
// this is the rest of the purl that needs to be parsed
560+
String remainder = uri.getRawPath();
561+
// trim trailing '/'
549562
int end = remainder.length() - 1;
550563
while (end > 0 && '/' == remainder.charAt(end)) {
551564
end--;
552565
}
553-
if (end < remainder.length() - 1) {
554-
remainder.setLength(end + 1);
555-
}
556-
int start = 0;
557-
while (start < remainder.length() && '/' == remainder.charAt(start)) {
558-
start++;
559-
}
560-
//there is no need for the "expensive" delete operation if the start is tracked and used throughout the rest
561-
// of the parsing.
562-
//if (start > 0) {
563-
// remainder.delete(0, start);
564-
//}
565-
566+
remainder = remainder.substring(0, end + 1);
567+
// there is exactly one leading '/' at this point
568+
start = 1;
566569
// type
567-
index = remainder.indexOf("/", start);
570+
int index = remainder.indexOf('/', start);
568571
if (index <= start) {
569572
throw new MalformedPackageURLException("Invalid purl: does not contain both a type and name");
570573
}
571574
this.type = validateType(remainder.substring(start, index).toLowerCase());
572-
//remainder.delete(0, index + 1);
573575
start = index + 1;
574576

575577
// version is optional - check for existence
576-
index = remainder.lastIndexOf("@");
578+
index = remainder.lastIndexOf('@');
577579
if (index >= start) {
578580
this.version = validateVersion(percentDecode(remainder.substring(index + 1)));
579-
remainder.setLength(index);
581+
remainder = remainder.substring(0, index);
580582
}
581583

582-
// The 'remainder' should now consist of the an optional namespace, and the name
583-
index = remainder.lastIndexOf("/");
584+
// The 'remainder' should now consist of an optional namespace and the name
585+
index = remainder.lastIndexOf('/');
584586
if (index <= start) {
585587
this.name = validateName(percentDecode(remainder.substring(start)));
586588
} else {
587589
this.name = validateName(percentDecode(remainder.substring(index + 1)));
588-
remainder.setLength(index);
590+
remainder = remainder.substring(0, index);
589591
this.namespace = validateNamespace(parsePath(remainder.substring(start), false));
590592
}
591593
verifyTypeConstraints(this.type, this.namespace, this.name);
@@ -672,8 +674,7 @@ public boolean isBaseEquals(final PackageURL purl) {
672674
* @since 1.4.0
673675
*/
674676
public boolean isCoordinatesEquals(final PackageURL purl) {
675-
return Objects.equals(scheme, purl.scheme) &&
676-
Objects.equals(type, purl.type) &&
677+
return Objects.equals(type, purl.type) &&
677678
Objects.equals(namespace, purl.namespace) &&
678679
Objects.equals(name, purl.name) &&
679680
Objects.equals(version, purl.version);
@@ -708,8 +709,7 @@ public boolean equals(Object o) {
708709
if (this == o) return true;
709710
if (o == null || getClass() != o.getClass()) return false;
710711
final PackageURL other = (PackageURL) o;
711-
return Objects.equals(scheme, other.scheme) &&
712-
Objects.equals(type, other.type) &&
712+
return Objects.equals(type, other.type) &&
713713
Objects.equals(namespace, other.namespace) &&
714714
Objects.equals(name, other.name) &&
715715
Objects.equals(version, other.version) &&
@@ -719,7 +719,7 @@ public boolean equals(Object o) {
719719

720720
@Override
721721
public int hashCode() {
722-
return Objects.hash(scheme, type, namespace, name, version, qualifiers, subpath);
722+
return Objects.hash(type, namespace, name, version, qualifiers, subpath);
723723
}
724724

725725
/**

0 commit comments

Comments
 (0)