Skip to content

Commit bdc4310

Browse files
committed
package-urlGH-188: Implement package type providers
1 parent f3762e6 commit bdc4310

36 files changed

+1544
-28
lines changed

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

Lines changed: 33 additions & 27 deletions
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,7 +78,7 @@ public final class PackageURL implements Serializable {
7778
private 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
*/
8384
private @Nullable String namespace;
@@ -181,7 +182,6 @@ public PackageURL(
181182
this.version = validateVersion(this.type, version);
182183
this.qualifiers = parseQualifiers(qualifiers);
183184
this.subpath = validateSubpath(subpath);
184-
verifyTypeConstraints(this.type, this.namespace, this.name);
185185
}
186186

187187
/**
@@ -277,11 +277,11 @@ private static String validateType(final String value) throws MalformedPackageUR
277277
return value;
278278
}
279279

280-
private static boolean isValidCharForType(int c) {
280+
public static boolean isValidCharForType(int c) {
281281
return (isAlphaNumeric(c) || c == '.' || c == '+' || c == '-');
282282
}
283283

284-
private static boolean isValidCharForKey(int c) {
284+
public static boolean isValidCharForKey(int c) {
285285
return (isAlphaNumeric(c) || c == '.' || c == '_' || c == '-');
286286
}
287287

@@ -455,6 +455,14 @@ private static void validateValue(final String key, final @Nullable String value
455455
}
456456
}
457457

458+
public PackageURL normalize() throws MalformedPackageURLException {
459+
System.out.println("Normalizing PackageURL " + type + " " + namespace + " " + name + " " + version + " "
460+
+ qualifiers + " " + subpath);
461+
PackageTypeFactory.getInstance().validateComponents(type, namespace, name, version, qualifiers, subpath);
462+
return PackageTypeFactory.getInstance()
463+
.normalizeComponents(type, namespace, name, version, qualifiers, subpath);
464+
}
465+
458466
/**
459467
* Returns the canonicalized representation of the purl.
460468
*
@@ -482,6 +490,17 @@ public String canonicalize() {
482490
* @since 1.3.2
483491
*/
484492
private String canonicalize(boolean coordinatesOnly) {
493+
try {
494+
PackageURL packageURL = normalize();
495+
namespace = packageURL.getNamespace();
496+
name = packageURL.getName();
497+
version = packageURL.getVersion();
498+
qualifiers = packageURL.getQualifiers();
499+
subpath = packageURL.getSubpath();
500+
} catch (MalformedPackageURLException e) {
501+
throw new ValidationException("Normalization failed", e);
502+
}
503+
485504
final StringBuilder purl = new StringBuilder();
486505
purl.append(SCHEME_PART).append(type).append('/');
487506
if (namespace != null) {
@@ -494,7 +513,7 @@ private String canonicalize(boolean coordinatesOnly) {
494513
}
495514

496515
if (!coordinatesOnly) {
497-
if (qualifiers != null) {
516+
if (!qualifiers.isEmpty()) {
498517
purl.append('?');
499518
Set<Map.Entry<String, String>> entries = qualifiers.entrySet();
500519
boolean separator = false;
@@ -523,18 +542,22 @@ private static boolean shouldEncode(int c) {
523542
return !isUnreserved(c);
524543
}
525544

526-
private static boolean isAlpha(int c) {
545+
public static boolean isAlpha(int c) {
527546
return (isLowerCase(c) || isUpperCase(c));
528547
}
529548

530549
private static boolean isDigit(int c) {
531550
return (c >= '0' && c <= '9');
532551
}
533552

534-
private static boolean isAlphaNumeric(int c) {
553+
public static boolean isAlphaNumeric(int c) {
535554
return (isDigit(c) || isAlpha(c));
536555
}
537556

557+
public static boolean isWhitespace(int c) {
558+
return (c == ' ' || c == '\t' || c == '\r' || c == '\n');
559+
}
560+
538561
private static boolean isUpperCase(int c) {
539562
return (c >= 'A' && c <= 'Z');
540563
}
@@ -559,7 +582,7 @@ private static int toLowerCase(int c) {
559582
return isUpperCase(c) ? (c ^ 0x20) : c;
560583
}
561584

562-
private static String toLowerCase(String s) {
585+
public static String toLowerCase(String s) {
563586
int pos = indexOfFirstUpperCaseChar(s);
564587

565588
if (pos == -1) {
@@ -765,28 +788,11 @@ private void parse(final String purl) throws MalformedPackageURLException {
765788
remainder = remainder.substring(0, index);
766789
this.namespace = validateNamespace(this.type, parsePath(remainder.substring(start), false));
767790
}
768-
verifyTypeConstraints(this.type, this.namespace, this.name);
769791
} catch (URISyntaxException e) {
770792
throw new MalformedPackageURLException("Invalid purl: " + e.getMessage(), e);
771793
}
772794
}
773795

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

11211127
private StandardTypes() {}
11221128
}

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

Lines changed: 4 additions & 0 deletions
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
}
Lines changed: 24 additions & 0 deletions
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 {}
Lines changed: 24 additions & 0 deletions
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 {}
Lines changed: 24 additions & 0 deletions
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 {}
Lines changed: 48 additions & 0 deletions
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+
}
Lines changed: 24 additions & 0 deletions
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 {}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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 java.util.Map;
26+
import org.jspecify.annotations.NonNull;
27+
import org.jspecify.annotations.Nullable;
28+
29+
public class ConanPackageTypeProvider implements PackageTypeProvider {
30+
@Override
31+
public void validateComponents(
32+
@NonNull String type,
33+
@Nullable String namespace,
34+
@NonNull String name,
35+
@Nullable String version,
36+
@Nullable Map<String, String> qualifiers,
37+
@Nullable String subpath)
38+
throws MalformedPackageURLException {
39+
boolean hasChannel = (qualifiers != null && !qualifiers.isEmpty());
40+
41+
if ((namespace != null && !namespace.isEmpty()) && !hasChannel) {
42+
throw new MalformedPackageURLException("invalid conan purl only namespace");
43+
} else if ((namespace == null || namespace.isEmpty()) && hasChannel) {
44+
throw new MalformedPackageURLException("invalid conan purl only channel qualifier");
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)