Skip to content

Commit d79efe2

Browse files
committed
package-urlGH-188: Implement package type providers
1 parent 062af5f commit d79efe2

36 files changed

+1549
-33
lines changed

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

+38-32
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323

2424
import static java.util.Objects.requireNonNull;
2525

26+
import com.github.packageurl.type.PackageTypeFactory;
2627
import java.io.Serializable;
2728
import java.net.URI;
2829
import java.net.URISyntaxException;
@@ -77,34 +78,34 @@ public final class PackageURL implements Serializable {
7778
private final String type;
7879

7980
/**
80-
* The name prefix such as a Maven groupid, a Docker image owner, a GitHub user or organization.
81+
* The name prefix such as a Maven groupId, a Docker image owner, a GitHub user or organization.
8182
* Optional and type-specific.
8283
*/
83-
private final @Nullable String namespace;
84+
private @Nullable String namespace;
8485

8586
/**
8687
* The name of the package.
8788
* Required.
8889
*/
89-
private final String name;
90+
private String name;
9091

9192
/**
9293
* The version of the package.
9394
* Optional.
9495
*/
95-
private final @Nullable String version;
96+
private @Nullable String version;
9697

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

103104
/**
104105
* Extra subpath within a package, relative to the package root.
105106
* Optional.
106107
*/
107-
private final @Nullable String subpath;
108+
private @Nullable String subpath;
108109

109110
/**
110111
* Constructs a new PackageURL object by parsing the specified string.
@@ -194,7 +195,6 @@ public PackageURL(final String purl) throws MalformedPackageURLException {
194195
remainder = remainder.substring(0, index);
195196
this.namespace = validateNamespace(this.type, parsePath(remainder.substring(start), false));
196197
}
197-
verifyTypeConstraints(this.type, this.namespace, this.name);
198198
} catch (URISyntaxException e) {
199199
throw new MalformedPackageURLException("Invalid purl: " + e.getMessage(), e);
200200
}
@@ -264,7 +264,6 @@ public PackageURL(
264264
this.version = validateVersion(this.type, version);
265265
this.qualifiers = parseQualifiers(qualifiers);
266266
this.subpath = validateSubpath(subpath);
267-
verifyTypeConstraints(this.type, this.namespace, this.name);
268267
}
269268

270269
/**
@@ -360,11 +359,11 @@ private static String validateType(final String value) throws MalformedPackageUR
360359
return value;
361360
}
362361

363-
private static boolean isValidCharForType(int c) {
362+
public static boolean isValidCharForType(int c) {
364363
return (isAlphaNumeric(c) || c == '.' || c == '+' || c == '-');
365364
}
366365

367-
private static boolean isValidCharForKey(int c) {
366+
public static boolean isValidCharForKey(int c) {
368367
return (isAlphaNumeric(c) || c == '.' || c == '_' || c == '-');
369368
}
370369

@@ -538,6 +537,14 @@ private static void validateValue(final String key, final @Nullable String value
538537
}
539538
}
540539

540+
public PackageURL normalize() throws MalformedPackageURLException {
541+
System.out.println("Normalizing PackageURL " + type + " " + namespace + " " + name + " " + version + " "
542+
+ qualifiers + " " + subpath);
543+
PackageTypeFactory.getInstance().validateComponents(type, namespace, name, version, qualifiers, subpath);
544+
return PackageTypeFactory.getInstance()
545+
.normalizeComponents(type, namespace, name, version, qualifiers, subpath);
546+
}
547+
541548
/**
542549
* Returns the canonicalized representation of the purl.
543550
*
@@ -565,6 +572,17 @@ public String canonicalize() {
565572
* @since 1.3.2
566573
*/
567574
private String canonicalize(boolean coordinatesOnly) {
575+
try {
576+
PackageURL packageURL = normalize();
577+
namespace = packageURL.getNamespace();
578+
name = packageURL.getName();
579+
version = packageURL.getVersion();
580+
qualifiers = packageURL.getQualifiers();
581+
subpath = packageURL.getSubpath();
582+
} catch (MalformedPackageURLException e) {
583+
throw new ValidationException("Normalization failed", e);
584+
}
585+
568586
final StringBuilder purl = new StringBuilder();
569587
purl.append(SCHEME_PART).append(type).append('/');
570588
if (namespace != null) {
@@ -577,7 +595,7 @@ private String canonicalize(boolean coordinatesOnly) {
577595
}
578596

579597
if (!coordinatesOnly) {
580-
if (qualifiers != null) {
598+
if (!qualifiers.isEmpty()) {
581599
purl.append('?');
582600
Set<Map.Entry<String, String>> entries = qualifiers.entrySet();
583601
boolean separator = false;
@@ -606,18 +624,22 @@ private static boolean shouldEncode(int c) {
606624
return !isUnreserved(c);
607625
}
608626

609-
private static boolean isAlpha(int c) {
627+
public static boolean isAlpha(int c) {
610628
return (isLowerCase(c) || isUpperCase(c));
611629
}
612630

613631
private static boolean isDigit(int c) {
614632
return (c >= '0' && c <= '9');
615633
}
616634

617-
private static boolean isAlphaNumeric(int c) {
635+
public static boolean isAlphaNumeric(int c) {
618636
return (isDigit(c) || isAlpha(c));
619637
}
620638

639+
public static boolean isWhitespace(int c) {
640+
return (c == ' ' || c == '\t' || c == '\r' || c == '\n');
641+
}
642+
621643
private static boolean isUpperCase(int c) {
622644
return (c >= 'A' && c <= 'Z');
623645
}
@@ -642,7 +664,7 @@ private static int toLowerCase(int c) {
642664
return isUpperCase(c) ? (c ^ 0x20) : c;
643665
}
644666

645-
static String toLowerCase(String s) {
667+
public static String toLowerCase(String s) {
646668
int pos = indexOfFirstUpperCaseChar(s);
647669

648670
if (pos == -1) {
@@ -770,22 +792,6 @@ static String percentEncode(final String source) {
770792
return changed ? new String(buffer.array(), 0, buffer.position(), StandardCharsets.UTF_8) : source;
771793
}
772794

773-
/**
774-
* Some purl types may have specific constraints. This method attempts to verify them.
775-
* @param type the purl type
776-
* @param namespace the purl namespace
777-
* @throws MalformedPackageURLException if constraints are not met
778-
*/
779-
private static void verifyTypeConstraints(String type, @Nullable String namespace, @Nullable String name)
780-
throws MalformedPackageURLException {
781-
if (StandardTypes.MAVEN.equals(type)) {
782-
if (isEmpty(namespace) || isEmpty(name)) {
783-
throw new MalformedPackageURLException(
784-
"The PackageURL specified is invalid. Maven requires both a namespace and name.");
785-
}
786-
}
787-
}
788-
789795
private static @Nullable Map<String, String> parseQualifiers(final @Nullable Map<String, String> qualifiers)
790796
throws MalformedPackageURLException {
791797
if (qualifiers == null || qualifiers.isEmpty()) {
@@ -1107,15 +1113,15 @@ public static final class StandardTypes {
11071113
* @deprecated use {@link #DEB} instead
11081114
*/
11091115
@Deprecated
1110-
public static final String DEBIAN = "deb";
1116+
public static final String DEBIAN = DEB;
11111117
/**
11121118
* Nixos packages.
11131119
*
11141120
* @since 1.1.0
11151121
* @deprecated use {@link #NIX} instead
11161122
*/
11171123
@Deprecated
1118-
public static final String NIXPKGS = "nix";
1124+
public static final String NIXPKGS = NIX;
11191125

11201126
private StandardTypes() {}
11211127
}

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

+4
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,8 @@ class ValidationException extends RuntimeException {
3838
ValidationException(String msg) {
3939
super(msg);
4040
}
41+
42+
ValidationException(String msg, Throwable cause) {
43+
super(msg, cause);
44+
}
4145
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* MIT License
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
* SOFTWARE.
21+
*/
22+
package com.github.packageurl.type;
23+
24+
public class ApkPackageTypeProvider extends LowercaseNamespaceAndNameTypeProvider {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* MIT License
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
* SOFTWARE.
21+
*/
22+
package com.github.packageurl.type;
23+
24+
public class BitbucketPackageTypeProvider extends LowercaseNamespaceAndNameTypeProvider {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* MIT License
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
* SOFTWARE.
21+
*/
22+
package com.github.packageurl.type;
23+
24+
public class BitnamiPackageTypeProvider extends LowercaseNamespacePackageTypeProvider {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/*
2+
* MIT License
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
* SOFTWARE.
21+
*/
22+
package com.github.packageurl.type;
23+
24+
import com.github.packageurl.MalformedPackageURLException;
25+
import com.github.packageurl.PackageURL;
26+
import java.util.Map;
27+
import org.jspecify.annotations.NonNull;
28+
import org.jspecify.annotations.Nullable;
29+
30+
public class CocoapodsPackageTypeProvider implements PackageTypeProvider {
31+
@Override
32+
public void validateComponents(
33+
@NonNull String type,
34+
@Nullable String namespace,
35+
@NonNull String name,
36+
@Nullable String version,
37+
@Nullable Map<String, String> qualifiers,
38+
@Nullable String subpath)
39+
throws MalformedPackageURLException {
40+
if (namespace != null && !namespace.isEmpty()) {
41+
throw new MalformedPackageURLException("invalid cocoapods purl cannot have a namespace");
42+
}
43+
44+
if (name.chars().anyMatch(PackageURL::isWhitespace) || name.startsWith(".") || name.contains("+")) {
45+
throw new MalformedPackageURLException("invalid cocoapods purl invalid name");
46+
}
47+
}
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
* MIT License
3+
*
4+
* Permission is hereby granted, free of charge, to any person obtaining a copy
5+
* of this software and associated documentation files (the "Software"), to deal
6+
* in the Software without restriction, including without limitation the rights
7+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8+
* copies of the Software, and to permit persons to whom the Software is
9+
* furnished to do so, subject to the following conditions:
10+
*
11+
* The above copyright notice and this permission notice shall be included in all
12+
* copies or substantial portions of the Software.
13+
*
14+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20+
* SOFTWARE.
21+
*/
22+
package com.github.packageurl.type;
23+
24+
public class ComposerPackageTypeProvider extends LowercaseNamespaceAndNameTypeProvider {}

0 commit comments

Comments
 (0)