├── .editorconfig
├── .gitattributes
├── .github
├── dependabot.yml
└── workflows
│ ├── maven.yml
│ └── semantic-pr.yml
├── .gitignore
├── .java-version
├── .mvn
└── jvm.config
├── LICENSE
├── README.md
├── bnd.bnd
├── pom.xml
├── release.sh
└── src
├── headers
├── java.txt
└── xml.txt
├── main
└── java
│ └── com
│ └── github
│ └── packageurl
│ ├── MalformedPackageURLException.java
│ ├── PackageURL.java
│ ├── PackageURLBuilder.java
│ ├── ValidationException.java
│ ├── internal
│ ├── StringUtil.java
│ └── package-info.java
│ ├── package-info.java
│ └── validator
│ ├── PackageURL.java
│ ├── PackageURLConstraintValidator.java
│ └── package-info.java
└── test
├── java
└── com
│ └── github
│ └── packageurl
│ ├── PackageURLBuilderTest.java
│ ├── PackageURLTest.java
│ ├── PurlParameters.java
│ └── internal
│ ├── StringUtilBenchmark.java
│ └── StringUtilTest.java
└── resources
├── components-constructor-only.json
├── custom-suite.json
├── string-constructor-only.json
├── test-suite-data.json
└── type-namespace-constructor-only.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_size = 2
7 | indent_style = space
8 | insert_final_newline = true
9 | max_line_length = 120
10 | tab_width = 2
11 | trim_trailing_whitespace = true
12 |
13 | [*.java]
14 | indent_size = 4
15 | tab_width = 4
16 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * -text
2 | .gitattributes text eol=lf
3 | .gitignore text eol=lf
4 | .java-version text eol=lf
5 | LICENSE text eol=lf
6 | *.config text eol=lf
7 | *.java text eol=lf
8 | *.json text eol=lf
9 | *.md text eol=lf
10 | *.sh text eol=lf
11 | *.xml text eol=lf
12 | *.yml text eol=lf
13 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "maven"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 |
8 | - package-ecosystem: "github-actions"
9 | directory: "/"
10 | schedule:
11 | interval: "daily"
12 |
--------------------------------------------------------------------------------
/.github/workflows/maven.yml:
--------------------------------------------------------------------------------
1 | name: Maven CI
2 |
3 | on: [push, pull_request]
4 |
5 | permissions: read-all
6 |
7 | jobs:
8 | build:
9 | strategy:
10 | matrix:
11 | os: [ ubuntu-latest ]
12 | java-version: [ 17 ]
13 | distro: [ 'zulu', 'temurin' ]
14 | runs-on: ${{ matrix.os }}
15 |
16 | steps:
17 |
18 | - name: Checkout repository
19 | uses: actions/checkout@v4.2.2
20 |
21 | - name: Set up JDK 8 and ${{ matrix.java-version }}
22 | uses: actions/setup-java@v4
23 | with:
24 | distribution: ${{ matrix.distro }}
25 | # We install two JDKs:
26 | # * The first one is used by tests through Maven Toolchains.
27 | # * The second one is used by Maven itself to perform the build.
28 | #
29 | # WARNING: The order matters.
30 | # The last version specified will be assigned to JAVA_HOME.
31 | java-version: |
32 | 8
33 | ${{ matrix.java-version }}
34 |
35 | - name: Build with Maven
36 | shell: bash
37 | run: mvn verify
38 |
--------------------------------------------------------------------------------
/.github/workflows/semantic-pr.yml:
--------------------------------------------------------------------------------
1 | name: "Semantic PR"
2 |
3 | on:
4 | pull_request_target:
5 | types:
6 | - opened
7 | - edited
8 | - synchronize
9 |
10 | permissions:
11 | contents: read
12 |
13 | jobs:
14 | main:
15 | name: Semantic PR title
16 | permissions:
17 | pull-requests: read
18 | statuses: write
19 | runs-on: ubuntu-latest
20 | steps:
21 | - uses: amannn/action-semantic-pull-request@0723387faaf9b38adef4775cd42cfd5155ed6017 # 5.5.3
22 | env:
23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
24 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | #### ignove maven
2 | #### Source: https://github.com/github/gitignore/blob/master/Maven.gitignore
3 | target/
4 | pom.xml.tag
5 | pom.xml.releaseBackup
6 | pom.xml.versionsBackup
7 | pom.xml.next
8 | release.properties
9 | dependency-reduced-pom.xml
10 | buildNumber.properties
11 | .mvn/timing.properties
12 | .mvn/wrapper/maven-wrapper.jar
13 | ##########################################################################
14 |
15 | #### ignore JetBrains IntelliJ
16 | .idea
17 | *.iml
18 | ##########################################################################
19 |
--------------------------------------------------------------------------------
/.java-version:
--------------------------------------------------------------------------------
1 | 17.0
2 |
--------------------------------------------------------------------------------
/.mvn/jvm.config:
--------------------------------------------------------------------------------
1 | --add-exports=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED
2 | --add-exports=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED
3 | --add-exports=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED
4 | --add-exports=jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
5 | --add-exports=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED
6 | --add-exports=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED
7 | --add-exports=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED
8 | --add-exports=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED
9 | --add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED
10 | --add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://github.com/package-url/packageurl-java/actions?workflow=Maven+CI)
2 | [](https://maven-badges.herokuapp.com/maven-central/com.github.package-url/packageurl-java)
3 | [![License][license-image]][license-url]
4 |
5 | # Package URL (purl) for Java
6 |
7 | This project implements a purl parser and class for Java.
8 |
9 | ## Requirements
10 |
11 | - The library has a **runtime** requirement of JRE 8 or newer.
12 | - To **build** the library from source, you need JDK 17 or newer.
13 |
14 | ## Compiling
15 |
16 | ```bash
17 | mvn clean install
18 | ````
19 |
20 | ## Maven Usage
21 | Package URL is available on the Maven Central Repository. These can be used without having to compile
22 | the project yourself.
23 |
24 | ```xml
25 |
Package-URL (aka purl) is a "mostly universal" URL to describe a package. A purl is a URL composed of seven components:
42 | *43 | * scheme:type/namespace/name@version?qualifiers#subpath 44 | *45 | *
46 | * Components are separated by a specific character for unambiguous parsing. 47 | * A purl must NOT contain a URL Authority, i.e., there is no support for username, 48 | * password, host and port components. A namespace segment may sometimes look 49 | * like a host, but its interpretation is specific to a type. 50 | *
51 | *SPEC: https://github.com/package-url/purl-spec
52 | * 53 | * @author Steve Springett 54 | * @since 1.0.0 55 | */ 56 | public final class PackageURL implements Serializable { 57 | private static final long serialVersionUID = 3243226021636427586L; 58 | 59 | /** 60 | * The PackageURL scheme constant 61 | */ 62 | public static final String SCHEME = "pkg"; 63 | 64 | /** 65 | * The PackageURL scheme ({@code "pkg"}) constant followed by a colon ({@code ':'}). 66 | */ 67 | private static final String SCHEME_PART = SCHEME + ':'; 68 | 69 | /** 70 | * The package "type" or package "protocol" such as maven, npm, nuget, gem, pypi, etc. 71 | * Required. 72 | */ 73 | private final String type; 74 | 75 | /** 76 | * The name prefix such as a Maven groupid, a Docker image owner, a GitHub user or organization. 77 | * Optional and type-specific. 78 | */ 79 | private final @Nullable String namespace; 80 | 81 | /** 82 | * The name of the package. 83 | * Required. 84 | */ 85 | private final String name; 86 | 87 | /** 88 | * The version of the package. 89 | * Optional. 90 | */ 91 | private final @Nullable String version; 92 | 93 | /** 94 | * Extra qualifying data for a package such as an OS, architecture, a distro, etc. 95 | * Optional and type-specific. 96 | */ 97 | private final @Nullable MapUse {@link PackageURL#isCoordinatesEquals} instead.
643 | * 644 | * @param purl the Package URL to evaluate 645 | * @return true if equivalence passes, false if not 646 | * @since 1.2.0 647 | */ 648 | // @Deprecated(since = "1.4.0", forRemoval = true) 649 | @Deprecated 650 | public boolean isBaseEquals(final PackageURL purl) { 651 | return isCoordinatesEquals(purl); 652 | } 653 | 654 | /** 655 | * Evaluates if the specified Package URL has the same values up to, but excluding 656 | * the qualifier (querystring). 657 | * This includes equivalence of the scheme, type, namespace, name, and version, but excludes qualifier and subpath 658 | * from evaluation. 659 | * 660 | * @param purl the Package URL to evaluate, not {@code null} 661 | * @return true if equivalence passes, false if not 662 | * @since 1.4.0 663 | */ 664 | public boolean isCoordinatesEquals(final PackageURL purl) { 665 | return type.equals(purl.type) 666 | && Objects.equals(namespace, purl.namespace) 667 | && name.equals(purl.name) 668 | && Objects.equals(version, purl.version); 669 | } 670 | 671 | /** 672 | * Returns only the canonicalized coordinates of the Package URL which includes the type, namespace, name, 673 | * and version, and which omits the qualifier and subpath. 674 | * @return A canonicalized PackageURL String excluding the qualifier and subpath. 675 | * @since 1.4.0 676 | */ 677 | public String getCoordinates() { 678 | return canonicalize(true); 679 | } 680 | 681 | /** 682 | * Evaluates if the specified Package URL has the same canonical value. This method 683 | * canonicalizes the Package URLs being evaluated and performs an equivalence on the 684 | * canonical values. Canonical equivalence is especially useful for qualifiers, which 685 | * can be in any order, but have a predictable order in canonicalized form. 686 | * 687 | * @param purl the Package URL to evaluate, not {@code null} 688 | * @return true if equivalence passes, false if not 689 | * @since 1.2.0 690 | */ 691 | public boolean isCanonicalEquals(final PackageURL purl) { 692 | return this.canonicalize().equals(purl.canonicalize()); 693 | } 694 | 695 | private static boolean isEmpty(@Nullable String value) { 696 | return value == null || value.isEmpty(); 697 | } 698 | 699 | @Override 700 | public boolean equals(@Nullable Object o) { 701 | if (this == o) return true; 702 | if (o == null || getClass() != o.getClass()) return false; 703 | final PackageURL other = (PackageURL) o; 704 | return type.equals(other.type) 705 | && Objects.equals(namespace, other.namespace) 706 | && name.equals(other.name) 707 | && Objects.equals(version, other.version) 708 | && Objects.equals(qualifiers, other.qualifiers) 709 | && Objects.equals(subpath, other.subpath); 710 | } 711 | 712 | @Override 713 | public int hashCode() { 714 | return Objects.hash(type, namespace, name, version, qualifiers, subpath); 715 | } 716 | 717 | /** 718 | * Convenience constants that defines common Package-URL 'type's. 719 | */ 720 | public static final class StandardTypes { 721 | /** 722 | * Arch Linux and other users of the libalpm/pacman package manager. 723 | * 724 | * @since 2.0.0 725 | */ 726 | public static final String ALPM = "alpm"; 727 | /** 728 | * APK-based packages. 729 | * 730 | * @since 2.0.0 731 | */ 732 | public static final String APK = "apk"; 733 | /** 734 | * Bitbucket-based packages. 735 | */ 736 | public static final String BITBUCKET = "bitbucket"; 737 | /** 738 | * Bitnami-based packages. 739 | * 740 | * @since 2.0.0 741 | */ 742 | public static final String BITNAMI = "bitnami"; 743 | /** 744 | * Rust. 745 | * 746 | * @since 1.2.0 747 | */ 748 | public static final String CARGO = "cargo"; 749 | /** 750 | * CocoaPods. 751 | * 752 | * @since 2.0.0 753 | */ 754 | public static final String COCOAPODS = "cocoapods"; 755 | /** 756 | * Composer PHP packages. 757 | */ 758 | public static final String COMPOSER = "composer"; 759 | /** 760 | * Conan C/C++ packages. 761 | * 762 | * @since 2.0.0 763 | */ 764 | public static final String CONAN = "conan"; 765 | /** 766 | * Conda packages. 767 | * 768 | * @since 2.0.0 769 | */ 770 | public static final String CONDA = "conda"; 771 | /** 772 | * CPAN Perl packages. 773 | * 774 | * @since 2.0.0 775 | */ 776 | public static final String CPAN = "cpan"; 777 | /** 778 | * CRAN R packages. 779 | * 780 | * @since 2.0.0 781 | */ 782 | public static final String CRAN = "cran"; 783 | /** 784 | * Debian, Debian derivatives, and Ubuntu packages. 785 | * 786 | * @since 2.0.0 787 | */ 788 | public static final String DEB = "deb"; 789 | /** 790 | * Docker images. 791 | */ 792 | public static final String DOCKER = "docker"; 793 | /** 794 | * RubyGems. 795 | */ 796 | public static final String GEM = "gem"; 797 | /** 798 | * Plain, generic packages that do not fit anywhere else, such as for "upstream-from-distro" packages. 799 | */ 800 | public static final String GENERIC = "generic"; 801 | /** 802 | * GitHub-based packages. 803 | */ 804 | public static final String GITHUB = "github"; 805 | /** 806 | * Go packages. 807 | */ 808 | public static final String GOLANG = "golang"; 809 | /** 810 | * Haskell packages. 811 | */ 812 | public static final String HACKAGE = "hackage"; 813 | /** 814 | * Hex packages. 815 | * 816 | * @since 2.0.0 817 | */ 818 | public static final String HEX = "hex"; 819 | /** 820 | * Hugging Face ML models. 821 | * 822 | * @since 2.0.0 823 | */ 824 | public static final String HUGGINGFACE = "huggingface"; 825 | /** 826 | * Lua packages installed with LuaRocks. 827 | * 828 | * @since 2.0.0 829 | */ 830 | public static final String LUAROCKS = "luarocks"; 831 | /** 832 | * Maven JARs and related artifacts. 833 | */ 834 | public static final String MAVEN = "maven"; 835 | /** 836 | * MLflow ML models (Azure ML, Databricks, etc.). 837 | * 838 | * @since 2.0.0 839 | */ 840 | public static final String MLFLOW = "mlflow"; 841 | /** 842 | * Nixos packages 843 | * 844 | * @since 2.0.0 845 | */ 846 | public static final String NIX = "nix"; 847 | /** 848 | * Node NPM packages. 849 | */ 850 | public static final String NPM = "npm"; 851 | /** 852 | * NuGet .NET packages. 853 | */ 854 | public static final String NUGET = "nuget"; 855 | /** 856 | * All artifacts stored in registries that conform to the 857 | * OCI Distribution Specification, including 858 | * container images built by Docker and others. 859 | * 860 | * @since 2.0.0 861 | */ 862 | public static final String OCI = "oci"; 863 | /** 864 | * Dart and Flutter packages. 865 | * 866 | * @since 2.0.0 867 | */ 868 | public static final String PUB = "pub"; 869 | /** 870 | * Python packages. 871 | */ 872 | public static final String PYPI = "pypi"; 873 | /** 874 | * QNX packages. 875 | * 876 | * @since 2.0.0 877 | */ 878 | public static final String QPKG = "qpkg"; 879 | /** 880 | * RPMs. 881 | */ 882 | public static final String RPM = "rpm"; 883 | /** 884 | * ISO-IEC 19770-2 Software Identification (SWID) tags. 885 | * 886 | * @since 2.0.0 887 | */ 888 | public static final String SWID = "swid"; 889 | /** 890 | * Swift packages. 891 | * 892 | * @since 2.0.0 893 | */ 894 | public static final String SWIFT = "swift"; 895 | /** 896 | * Debian, Debian derivatives, and Ubuntu packages. 897 | * 898 | * @deprecated use {@link #DEB} instead 899 | */ 900 | @Deprecated 901 | public static final String DEBIAN = "deb"; 902 | /** 903 | * Nixos packages. 904 | * 905 | * @since 1.1.0 906 | * @deprecated use {@link #NIX} instead 907 | */ 908 | @Deprecated 909 | public static final String NIXPKGS = "nix"; 910 | 911 | private StandardTypes() {} 912 | } 913 | } 914 | -------------------------------------------------------------------------------- /src/main/java/com/github/packageurl/PackageURLBuilder.java: -------------------------------------------------------------------------------- 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; 23 | 24 | import static java.util.Objects.requireNonNull; 25 | 26 | import java.util.Collections; 27 | import java.util.Map; 28 | import java.util.Set; 29 | import java.util.TreeMap; 30 | import org.jspecify.annotations.Nullable; 31 | 32 | /** 33 | * A builder construct for Package-URL objects. 34 | * 35 | * @since 1.1.0 36 | */ 37 | public final class PackageURLBuilder { 38 | private @Nullable String type = null; 39 | private @Nullable String namespace = null; 40 | private @Nullable String name = null; 41 | private @Nullable String version = null; 42 | private @Nullable String subpath = null; 43 | private @Nullable Map157 | * If {@code value} is empty or {@code null}, the given qualifier is removed instead. 158 | *
159 | * 160 | * @param key the package qualifier key, not {@code null} 161 | * @param value the package qualifier value or {@code null} 162 | * @return a reference to the builder 163 | * @throws NullPointerException if {@code key} is {@code null} 164 | * @see PackageURL#getQualifiers() 165 | */ 166 | public PackageURLBuilder withQualifier(final String key, final @Nullable String value) { 167 | requireNonNull(key, "qualifier key can not be null"); 168 | if (value == null || value.isEmpty()) { 169 | if (qualifiers != null) { 170 | qualifiers.remove(key); 171 | } 172 | } else { 173 | if (qualifiers == null) { 174 | qualifiers = new TreeMap<>(); 175 | } 176 | qualifiers.put(requireNonNull(key, "qualifier key can not be null"), value); 177 | } 178 | return this; 179 | } 180 | 181 | /** 182 | * Adds the package qualifiers. 183 | * 184 | * @param qualifiers the package qualifiers, or {@code null} 185 | * @return a reference to the builder 186 | * @see PackageURL#getQualifiers() 187 | * @since 2.0.0 188 | */ 189 | public PackageURLBuilder withQualifiers(final @Nullable Map189 | * Warning: Profiling shows that the performance of {@link #percentEncode} relies heavily on this method. 190 | * Modify with care. 191 | *
192 | * @param c non-negative integer. 193 | */ 194 | private static boolean isUnreserved(int c) { 195 | return c < 128 && UNRESERVED_CHARS[c]; 196 | } 197 | 198 | /** 199 | * @param c non-negative integer 200 | */ 201 | private static boolean shouldEncode(int c) { 202 | return !isUnreserved(c); 203 | } 204 | 205 | private static boolean shouldEncode(String s) { 206 | for (int i = 0, length = s.length(); i < length; i++) { 207 | if (shouldEncode(s.charAt(i))) { 208 | return true; 209 | } 210 | } 211 | return false; 212 | } 213 | 214 | private static boolean isAlpha(int c) { 215 | return (isLowerCase(c) || isUpperCase(c)); 216 | } 217 | 218 | private static boolean isAlphaNumeric(int c) { 219 | return (isDigit(c) || isAlpha(c)); 220 | } 221 | 222 | private static boolean isUpperCase(int c) { 223 | return 'A' <= c && c <= 'Z'; 224 | } 225 | 226 | private static boolean isLowerCase(int c) { 227 | return (c >= 'a' && c <= 'z'); 228 | } 229 | 230 | private static int toLowerCase(int c) { 231 | return isUpperCase(c) ? (c ^ 0x20) : c; 232 | } 233 | 234 | private static int indexOfFirstUpperCaseChar(String s) { 235 | for (int i = 0, length = s.length(); i < length; i++) { 236 | if (isUpperCase(s.charAt(i))) { 237 | return i; 238 | } 239 | } 240 | return -1; 241 | } 242 | 243 | private static int indexOfFirstPercentChar(final byte[] bytes) { 244 | for (int i = 0, length = bytes.length; i < length; i++) { 245 | if (bytes[i] == PERCENT_CHAR) { 246 | return i; 247 | } 248 | } 249 | return -1; 250 | } 251 | 252 | private static byte percentDecode(final byte[] bytes, final int start) { 253 | if (start + 2 >= bytes.length) { 254 | throw new ValidationException("Incomplete percent encoding at offset " + start + " with value '" 255 | + new String(bytes, start, bytes.length - start, StandardCharsets.UTF_8) + "'"); 256 | } 257 | 258 | int pos1 = start + 1; 259 | byte b1 = bytes[pos1]; 260 | int c1 = Character.digit(b1, 16); 261 | 262 | if (c1 == -1) { 263 | throw new ValidationException( 264 | "Invalid percent encoding char 1 at offset " + pos1 + " with value '" + ((char) b1) + "'"); 265 | } 266 | 267 | int pos2 = pos1 + 1; 268 | byte b2 = bytes[pos2]; 269 | int c2 = Character.digit(bytes[pos2], 16); 270 | 271 | if (c2 == -1) { 272 | throw new ValidationException( 273 | "Invalid percent encoding char 2 at offset " + pos2 + " with value '" + ((char) b2) + "'"); 274 | } 275 | 276 | return ((byte) ((c1 << 4) + c2)); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /src/main/java/com/github/packageurl/internal/package-info.java: -------------------------------------------------------------------------------- 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 | 23 | /** 24 | * This package contains utility classes used by the PackageURL library. 25 | */ 26 | @NullMarked 27 | package com.github.packageurl.internal; 28 | 29 | import org.jspecify.annotations.NullMarked; 30 | -------------------------------------------------------------------------------- /src/main/java/com/github/packageurl/package-info.java: -------------------------------------------------------------------------------- 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 | 23 | /** 24 | *Java implementation of the Package-URL Specification.
25 | *https://github.com/package-url/purl-spec
26 | */ 27 | @NullMarked 28 | @Export 29 | package com.github.packageurl; 30 | 31 | import org.jspecify.annotations.NullMarked; 32 | import org.osgi.annotation.bundle.Export; 33 | -------------------------------------------------------------------------------- /src/main/java/com/github/packageurl/validator/PackageURL.java: -------------------------------------------------------------------------------- 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.validator; 23 | 24 | import jakarta.validation.Constraint; 25 | import jakarta.validation.Payload; 26 | import java.lang.annotation.ElementType; 27 | import java.lang.annotation.Retention; 28 | import java.lang.annotation.RetentionPolicy; 29 | import java.lang.annotation.Target; 30 | 31 | /** 32 | * Annotation that can be applied to a String field within a class. The PackageURL annotation instructs 33 | * the JSR-303 compliant validator to validate the field to ensure it meets the Package URL specification. 34 | * @since 1.3.0 35 | */ 36 | @Target({ElementType.FIELD}) 37 | @Retention(RetentionPolicy.RUNTIME) 38 | @Constraint(validatedBy = PackageURLConstraintValidator.class) 39 | public @interface PackageURL { 40 | /** 41 | * Gets the error message. 42 | * 43 | * @return the error message 44 | */ 45 | String message() default 46 | "The Package URL (purl) must be a valid URI and conform to https://github.com/package-url/purl-spec"; 47 | 48 | /** 49 | * Gets the validation groups. 50 | * 51 | * @return the validation groups 52 | */ 53 | Class>[] groups() default {}; 54 | 55 | /** 56 | * Gets the payload for the constraint. 57 | * 58 | * @return the payload for the constraint 59 | */ 60 | Class extends Payload>[] payload() default {}; 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/github/packageurl/validator/PackageURLConstraintValidator.java: -------------------------------------------------------------------------------- 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.validator; 23 | 24 | import com.github.packageurl.MalformedPackageURLException; 25 | import jakarta.validation.ConstraintValidator; 26 | import jakarta.validation.ConstraintValidatorContext; 27 | import org.jspecify.annotations.Nullable; 28 | 29 | /** 30 | * A JSR-303 compliant validator that validates String fields conform to the Package URL specification. 31 | * @since 1.3.0 32 | */ 33 | public class PackageURLConstraintValidator implements ConstraintValidator
45 | * Original test cases retrieved from:
46 | * https://raw.githubusercontent.com/package-url/purl-spec/master/test-suite-data.json
47 | *
48 | * @author Steve Springett
49 | */
50 | class PackageURLTest {
51 | private static final Locale DEFAULT_LOCALE = Locale.getDefault();
52 |
53 | @BeforeAll
54 | static void setup() {
55 | Locale.setDefault(new Locale("tr"));
56 | }
57 |
58 | @AfterAll
59 | static void resetLocale() {
60 | Locale.setDefault(DEFAULT_LOCALE);
61 | }
62 |
63 | @Test
64 | void validPercentEncoding() throws MalformedPackageURLException {
65 | PackageURL purl = new PackageURL("maven", "com.google.summit", "summit-ast", "2.2.0\n", null, null);
66 | assertEquals("pkg:maven/com.google.summit/summit-ast@2.2.0%0A", purl.toString());
67 | PackageURL purl2 =
68 | new PackageURL("pkg:nuget/%D0%9Cicros%D0%BEft.%D0%95ntit%D1%83Fram%D0%B5work%D0%A1%D0%BEr%D0%B5");
69 | assertEquals("Мicrosоft.ЕntitуFramеworkСоrе", purl2.getName());
70 | assertEquals(
71 | "pkg:nuget/%D0%9Cicros%D0%BEft.%D0%95ntit%D1%83Fram%D0%B5work%D0%A1%D0%BEr%D0%B5", purl2.toString());
72 | }
73 |
74 | @Test
75 | void invalidPercentEncoding() {
76 | assertThrowsExactly(
77 | MalformedPackageURLException.class,
78 | () -> new PackageURL("pkg:maven/com.google.summit/summit-ast@2.2.0%"));
79 | assertThrowsExactly(
80 | MalformedPackageURLException.class,
81 | () -> new PackageURL("pkg:maven/com.google.summit/summit-ast@2.2.0%0"));
82 | }
83 |
84 | static Stream
41 | * Run the benchmark with:
42 | *
47 | * To pass arguments to JMH use:
48 | *
60 | *
65 | */
66 | private static Arguments createTestDefinition(JSONObject testDefinition) {
67 | return Arguments.of(
68 | testDefinition.getString("description"),
69 | testDefinition.optString("purl"),
70 | new PurlParameters(
71 | testDefinition.optString("type", null),
72 | testDefinition.optString("namespace", null),
73 | testDefinition.optString("name", null),
74 | testDefinition.optString("version", null),
75 | testDefinition.optJSONObject("qualifiers"),
76 | testDefinition.optString("subpath", null)),
77 | testDefinition.optString("canonical_purl"),
78 | testDefinition.getBoolean("is_invalid"));
79 | }
80 |
81 | private final @Nullable String type;
82 | private final @Nullable String namespace;
83 | private final @Nullable String name;
84 | private final @Nullable String version;
85 | private final Map
44 | * mvn -Pbenchmark
45 | *
46 | *
50 | * mvn -Pbenchmark -Djmh.args="
52 | */
53 | @BenchmarkMode(Mode.AverageTime)
54 | @OutputTimeUnit(TimeUnit.MICROSECONDS)
55 | @State(Scope.Benchmark)
56 | public class StringUtilBenchmark {
57 |
58 | private static final int DATA_COUNT = 1000;
59 | private static final int DECODED_LENGTH = 256;
60 | private static final byte[] UNRESERVED =
61 | "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~".getBytes(StandardCharsets.US_ASCII);
62 |
63 | @Param({"0", "0.1", "0.5"})
64 | private double nonAsciiProb;
65 |
66 | private String[] decodedData;
67 | private String[] encodedData;
68 |
69 | @Setup
70 | public void setup() {
71 | decodedData = createDecodedData();
72 | encodedData = encodeData(decodedData);
73 | }
74 |
75 | private String[] createDecodedData() {
76 | Random random = new Random();
77 | String[] decodedData = new String[DATA_COUNT];
78 | for (int i = 0; i < DATA_COUNT; i++) {
79 | char[] chars = new char[DECODED_LENGTH];
80 | for (int j = 0; j < DECODED_LENGTH; j++) {
81 | if (random.nextDouble() < nonAsciiProb) {
82 | chars[j] = (char) (Byte.MAX_VALUE + 1 + random.nextInt(Short.MAX_VALUE - Byte.MAX_VALUE - 1));
83 | } else {
84 | chars[j] = (char) UNRESERVED[random.nextInt(UNRESERVED.length)];
85 | }
86 | }
87 | decodedData[i] = new String(chars);
88 | }
89 | return decodedData;
90 | }
91 |
92 | private static String[] encodeData(String[] decodedData) {
93 | String[] encodedData = new String[decodedData.length];
94 | for (int i = 0; i < encodedData.length; i++) {
95 | encodedData[i] = StringUtil.percentEncode(decodedData[i]);
96 | if (!StringUtil.percentDecode(encodedData[i]).equals(decodedData[i])) {
97 | throw new RuntimeException(
98 | "Invalid implementation of `percentEncode` and `percentDecode`.\nOriginal data: "
99 | + encodedData[i] + "\nEncoded and decoded data: "
100 | + StringUtil.percentDecode(encodedData[i]));
101 | }
102 | }
103 | return encodedData;
104 | }
105 |
106 | @Benchmark
107 | public void baseline(Blackhole blackhole) {
108 | for (int i = 0; i < DATA_COUNT; i++) {
109 | byte[] buffer = decodedData[i].getBytes(StandardCharsets.UTF_8);
110 | // Prevent JIT compiler from assuming the buffer was not modified
111 | for (int idx = 0; idx < buffer.length; idx++) {
112 | buffer[idx] ^= 0x20;
113 | }
114 | blackhole.consume(new String(buffer, StandardCharsets.UTF_8));
115 | }
116 | }
117 |
118 | @Benchmark
119 | public void toLowerCaseJre(Blackhole blackhole) {
120 | for (int i = 0; i < DATA_COUNT; i++) {
121 | blackhole.consume(decodedData[i].toLowerCase(Locale.ROOT));
122 | }
123 | }
124 |
125 | @Benchmark
126 | public void toLowerCase(Blackhole blackhole) {
127 | for (int i = 0; i < DATA_COUNT; i++) {
128 | blackhole.consume(StringUtil.toLowerCase(decodedData[i]));
129 | }
130 | }
131 |
132 | @Benchmark
133 | public void percentDecode(final Blackhole blackhole) {
134 | for (int i = 0; i < DATA_COUNT; i++) {
135 | blackhole.consume(StringUtil.percentDecode(encodedData[i]));
136 | }
137 | }
138 |
139 | @Benchmark
140 | public void percentEncode(final Blackhole blackhole) {
141 | for (int i = 0; i < DATA_COUNT; i++) {
142 | blackhole.consume(StringUtil.percentEncode(decodedData[i]));
143 | }
144 | }
145 | }
146 |
--------------------------------------------------------------------------------
/src/test/java/com/github/packageurl/internal/StringUtilTest.java:
--------------------------------------------------------------------------------
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.internal;
23 |
24 | import static org.junit.jupiter.api.Assertions.assertEquals;
25 | import static org.junit.jupiter.api.Assertions.assertThrowsExactly;
26 |
27 | import com.github.packageurl.ValidationException;
28 | import org.junit.jupiter.api.Test;
29 |
30 | class StringUtilTest {
31 | @Test
32 | void invalidPercentEncoding() {
33 | Throwable t1 = assertThrowsExactly(ValidationException.class, () -> StringUtil.percentDecode("a%0"));
34 | assertEquals("Incomplete percent encoding at offset 1 with value '%0'", t1.getMessage());
35 | Throwable t2 = assertThrowsExactly(ValidationException.class, () -> StringUtil.percentDecode("aaaa%%0A"));
36 | assertEquals("Invalid percent encoding char 1 at offset 5 with value '%'", t2.getMessage());
37 | Throwable t3 = assertThrowsExactly(ValidationException.class, () -> StringUtil.percentDecode("%0G"));
38 | assertEquals("Invalid percent encoding char 2 at offset 2 with value 'G'", t3.getMessage());
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/test/resources/components-constructor-only.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "description": "minimal valid PURL",
4 | "type": "my.type-9+",
5 | "name": "name",
6 | "canonical_purl": "pkg:my.type-9+/name",
7 | "is_invalid": false
8 | },
9 | {
10 | "description": "PURL with all components",
11 | "type": "type",
12 | "namespace": "namespace",
13 | "name": "name",
14 | "version": "version",
15 | "qualifiers": {
16 | "key": "value"
17 | },
18 | "subpath": "subpath",
19 | "canonical_purl": "pkg:type/namespace/name@version?key=value#subpath",
20 | "is_invalid": false
21 | },
22 | {
23 | "description": "Non alphanumeric key",
24 | "type": "generic",
25 | "namespace": "namespace",
26 | "name": "name",
27 | "version": "version",
28 | "qualifiers": {
29 | "key_1.1-": "value"
30 | },
31 | "subpath": "subpath",
32 | "canonical_purl": "pkg:generic/namespace/name@version?key_1.1-=value#subpath",
33 | "is_invalid": false
34 | },
35 | {
36 | "description": "Two qualifiers",
37 | "type": "generic",
38 | "namespace": "",
39 | "name": "name",
40 | "version": "version",
41 | "qualifiers": {
42 | "key": "value",
43 | "next": "value"
44 | },
45 | "subpath": "",
46 | "canonical_purl": "pkg:generic/name@version?key=value&next=value",
47 | "is_invalid": false
48 | }
49 | ]
50 |
--------------------------------------------------------------------------------
/src/test/resources/custom-suite.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "description": "empty fragment",
4 | "purl": "pkg:generic/namespace/name@1.0.0#",
5 | "type": "generic",
6 | "namespace": "namespace",
7 | "name": "name",
8 | "version": "1.0.0",
9 | "canonical_purl": "pkg:generic/namespace/name@1.0.0",
10 | "is_invalid": false
11 | },
12 | {
13 | "description": "value with `=`",
14 | "purl": "pkg:generic/namespace/name@1.0.0?key=value==",
15 | "type": "generic",
16 | "namespace": "namespace",
17 | "name": "name",
18 | "version": "1.0.0",
19 | "qualifiers": {
20 | "key": "value=="
21 | },
22 | "canonical_purl": "pkg:generic/namespace/name@1.0.0?key=value%3D%3D",
23 | "is_invalid": false
24 | },
25 | {
26 | "description": "everything null",
27 | "is_invalid": true
28 | }
29 | ]
30 |
--------------------------------------------------------------------------------
/src/test/resources/string-constructor-only.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "description": "constructor with `invalid/%2F/subpath` should have thrown an error",
4 | "purl": "pkg:GOLANG/google.golang.org/genproto@abcdedf#invalid/%2F/subpath",
5 | "is_invalid": true
6 | },
7 | {
8 | "description": "constructor with null purl",
9 | "purl": null,
10 | "is_invalid": true
11 | },
12 | {
13 | "description": "constructor with empty purl",
14 | "purl": "",
15 | "is_invalid": true
16 | },
17 | {
18 | "description": "constructor with port number",
19 | "purl": "pkg://generic:8080/name",
20 | "is_invalid": true
21 | },
22 | {
23 | "description": "constructor with username",
24 | "purl": "pkg://user@generic/name",
25 | "is_invalid": true
26 | },
27 | {
28 | "description": "constructor with invalid url",
29 | "purl": "invalid url",
30 | "is_invalid": true
31 | },
32 | {
33 | "description": "constructor with url with duplicate qualifiers",
34 | "purl": "pkg://generic/name?key=one&key=two",
35 | "is_invalid": true
36 | },
37 | {
38 | "description": "constructor with url with duplicate qualifiers",
39 | "purl": "pkg://generic/name?key=one&KEY=two",
40 | "is_invalid": true
41 | },
42 | {
43 | "description": "constructor with upper case key",
44 | "purl": "pkg://generic/name?KEY=one",
45 | "type": "generic",
46 | "name": "name",
47 | "qualifiers": {
48 | "key": "one"
49 | },
50 | "canonical_purl": "pkg:generic/name?key=one",
51 | "is_invalid": false
52 | },
53 | {
54 | "description": "constructor with empty key",
55 | "purl": "pkg://generic/name?KEY=",
56 | "type": "generic",
57 | "name": "name",
58 | "canonical_purl": "pkg:generic/name",
59 | "is_invalid": false
60 | },
61 | {
62 | "description": "constructor with null key",
63 | "purl": "pkg://generic/name?KEY",
64 | "type": "generic",
65 | "name": "name",
66 | "canonical_purl": "pkg:generic/name",
67 | "is_invalid": false
68 | }
69 | ]
70 |
--------------------------------------------------------------------------------
/src/test/resources/test-suite-data.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "description": "valid maven purl",
4 | "purl": "pkg:maven/org.apache.commons/io@1.3.4",
5 | "canonical_purl": "pkg:maven/org.apache.commons/io@1.3.4",
6 | "type": "maven",
7 | "namespace": "org.apache.commons",
8 | "name": "io",
9 | "version": "1.3.4",
10 | "qualifiers": null,
11 | "subpath": null,
12 | "is_invalid": false
13 | },
14 | {
15 | "description": "basic valid maven purl without version",
16 | "purl": "pkg:maven/org.apache.commons/io",
17 | "canonical_purl": "pkg:maven/org.apache.commons/io",
18 | "type": "maven",
19 | "namespace": "org.apache.commons",
20 | "name": "io",
21 | "version": null,
22 | "qualifiers": null,
23 | "subpath": null,
24 | "is_invalid": false
25 | },
26 | {
27 | "description": "valid go purl without version and with subpath",
28 | "purl": "pkg:GOLANG/google.golang.org/genproto#/googleapis/api/annotations/",
29 | "canonical_purl": "pkg:golang/google.golang.org/genproto#googleapis/api/annotations",
30 | "type": "golang",
31 | "namespace": "google.golang.org",
32 | "name": "genproto",
33 | "version": null,
34 | "qualifiers": null,
35 | "subpath": "googleapis/api/annotations",
36 | "is_invalid": false
37 | },
38 | {
39 | "description": "valid go purl with version and subpath",
40 | "purl": "pkg:GOLANG/google.golang.org/genproto@abcdedf#/googleapis/api/annotations/",
41 | "canonical_purl": "pkg:golang/google.golang.org/genproto@abcdedf#googleapis/api/annotations",
42 | "type": "golang",
43 | "namespace": "google.golang.org",
44 | "name": "genproto",
45 | "version": "abcdedf",
46 | "qualifiers": null,
47 | "subpath": "googleapis/api/annotations",
48 | "is_invalid": false
49 | },
50 | {
51 | "description": "bitbucket namespace and name should be lowercased",
52 | "purl": "pkg:bitbucket/birKenfeld/pyGments-main@244fd47e07d1014f0aed9c",
53 | "canonical_purl": "pkg:bitbucket/birkenfeld/pygments-main@244fd47e07d1014f0aed9c",
54 | "type": "bitbucket",
55 | "namespace": "birkenfeld",
56 | "name": "pygments-main",
57 | "version": "244fd47e07d1014f0aed9c",
58 | "qualifiers": null,
59 | "subpath": null,
60 | "is_invalid": false
61 | },
62 | {
63 | "description": "github namespace and name should be lowercased",
64 | "purl": "pkg:github/Package-url/purl-Spec@244fd47e07d1004f0aed9c",
65 | "canonical_purl": "pkg:github/package-url/purl-spec@244fd47e07d1004f0aed9c",
66 | "type": "github",
67 | "namespace": "package-url",
68 | "name": "purl-spec",
69 | "version": "244fd47e07d1004f0aed9c",
70 | "qualifiers": null,
71 | "subpath": null,
72 | "is_invalid": false
73 | },
74 | {
75 | "description": "debian can use qualifiers",
76 | "purl": "pkg:deb/debian/curl@7.50.3-1?arch=i386&distro=jessie",
77 | "canonical_purl": "pkg:deb/debian/curl@7.50.3-1?arch=i386&distro=jessie",
78 | "type": "deb",
79 | "namespace": "debian",
80 | "name": "curl",
81 | "version": "7.50.3-1",
82 | "qualifiers": {"arch": "i386", "distro": "jessie"},
83 | "subpath": null,
84 | "is_invalid": false
85 | },
86 | {
87 | "description": "docker uses qualifiers and hash image id as versions",
88 | "purl": "pkg:docker/customer/dockerimage@sha256:244fd47e07d1004f0aed9c?repository_url=gcr.io",
89 | "canonical_purl": "pkg:docker/customer/dockerimage@sha256%3A244fd47e07d1004f0aed9c?repository_url=gcr.io",
90 | "type": "docker",
91 | "namespace": "customer",
92 | "name": "dockerimage",
93 | "version": "sha256:244fd47e07d1004f0aed9c",
94 | "qualifiers": {"repository_url": "gcr.io"},
95 | "subpath": null,
96 | "is_invalid": false
97 | },
98 | {
99 | "description": "Java gem can use a qualifier",
100 | "purl": "pkg:gem/jruby-launcher@1.1.2?Platform=java",
101 | "canonical_purl": "pkg:gem/jruby-launcher@1.1.2?platform=java",
102 | "type": "gem",
103 | "namespace": null,
104 | "name": "jruby-launcher",
105 | "version": "1.1.2",
106 | "qualifiers": {"platform": "java"},
107 | "subpath": null,
108 | "is_invalid": false
109 | },
110 | {
111 | "description": "maven often uses qualifiers",
112 | "purl": "pkg:Maven/org.apache.xmlgraphics/batik-anim@1.9.1?repositorY_url=repo.spring.io/release&classifier=sources",
113 | "canonical_purl": "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?classifier=sources&repository_url=repo.spring.io%2Frelease",
114 | "type": "maven",
115 | "namespace": "org.apache.xmlgraphics",
116 | "name": "batik-anim",
117 | "version": "1.9.1",
118 | "qualifiers": {"classifier": "sources", "repository_url": "repo.spring.io/release"},
119 | "subpath": null,
120 | "is_invalid": false
121 | },
122 | {
123 | "description": "maven pom reference",
124 | "purl": "pkg:Maven/org.apache.xmlgraphics/batik-anim@1.9.1?repositorY_url=repo.spring.io/release&extension=pom",
125 | "canonical_purl": "pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?extension=pom&repository_url=repo.spring.io%2Frelease",
126 | "type": "maven",
127 | "namespace": "org.apache.xmlgraphics",
128 | "name": "batik-anim",
129 | "version": "1.9.1",
130 | "qualifiers": {"extension": "pom", "repository_url": "repo.spring.io/release"},
131 | "subpath": null,
132 | "is_invalid": false
133 | },
134 | {
135 | "description": "maven can come with a type qualifier",
136 | "purl": "pkg:Maven/net.sf.jacob-project/jacob@1.14.3?type=dll&classifier=x86",
137 | "canonical_purl": "pkg:maven/net.sf.jacob-project/jacob@1.14.3?classifier=x86&type=dll",
138 | "type": "maven",
139 | "namespace": "net.sf.jacob-project",
140 | "name": "jacob",
141 | "version": "1.14.3",
142 | "qualifiers": {"classifier": "x86", "type": "dll"},
143 | "subpath": null,
144 | "is_invalid": false
145 | },
146 | {
147 | "description": "npm can be scoped",
148 | "purl": "pkg:npm/%40angular/animation@12.3.1",
149 | "canonical_purl": "pkg:npm/%40angular/animation@12.3.1",
150 | "type": "npm",
151 | "namespace": "@angular",
152 | "name": "animation",
153 | "version": "12.3.1",
154 | "qualifiers": null,
155 | "subpath": null,
156 | "is_invalid": false
157 | },
158 | {
159 | "description": "nuget names are case sensitive",
160 | "purl": "pkg:Nuget/EnterpriseLibrary.Common@6.0.1304",
161 | "canonical_purl": "pkg:nuget/EnterpriseLibrary.Common@6.0.1304",
162 | "type": "nuget",
163 | "namespace": null,
164 | "name": "EnterpriseLibrary.Common",
165 | "version": "6.0.1304",
166 | "qualifiers": null,
167 | "subpath": null,
168 | "is_invalid": false
169 | },
170 | {
171 | "description": "pypi names have special rules and not case sensitive",
172 | "purl": "pkg:PYPI/Django_package@1.11.1.dev1",
173 | "canonical_purl": "pkg:pypi/django-package@1.11.1.dev1",
174 | "type": "pypi",
175 | "namespace": null,
176 | "name": "django-package",
177 | "version": "1.11.1.dev1",
178 | "qualifiers": null,
179 | "subpath": null,
180 | "is_invalid": false
181 | },
182 | {
183 | "description": "rpm often use qualifiers",
184 | "purl": "pkg:Rpm/fedora/curl@7.50.3-1.fc25?Arch=i386&Distro=fedora-25",
185 | "canonical_purl": "pkg:rpm/fedora/curl@7.50.3-1.fc25?arch=i386&distro=fedora-25",
186 | "type": "rpm",
187 | "namespace": "fedora",
188 | "name": "curl",
189 | "version": "7.50.3-1.fc25",
190 | "qualifiers": {"arch": "i386", "distro": "fedora-25"},
191 | "subpath": null,
192 | "is_invalid": false
193 | },
194 | {
195 | "description": "a scheme is always required",
196 | "purl": "EnterpriseLibrary.Common@6.0.1304",
197 | "canonical_purl": "EnterpriseLibrary.Common@6.0.1304",
198 | "type": null,
199 | "namespace": null,
200 | "name": "EnterpriseLibrary.Common",
201 | "version": null,
202 | "qualifiers": null,
203 | "subpath": null,
204 | "is_invalid": true
205 | },
206 | {
207 | "description": "a type is always required",
208 | "purl": "pkg:EnterpriseLibrary.Common@6.0.1304",
209 | "canonical_purl": "pkg:EnterpriseLibrary.Common@6.0.1304",
210 | "type": null,
211 | "namespace": null,
212 | "name": "EnterpriseLibrary.Common",
213 | "version": null,
214 | "qualifiers": null,
215 | "subpath": null,
216 | "is_invalid": true
217 | },
218 | {
219 | "description": "a name is required",
220 | "purl": "pkg:maven/@1.3.4",
221 | "canonical_purl": "pkg:maven/@1.3.4",
222 | "type": "maven",
223 | "namespace": null,
224 | "name": null,
225 | "version": null,
226 | "qualifiers": null,
227 | "subpath": null,
228 | "is_invalid": true
229 | },
230 | {
231 | "description": "a namespace is required",
232 | "purl": "pkg:maven/io@1.3.4",
233 | "canonical_purl": "pkg:maven/io@1.3.4",
234 | "type": "maven",
235 | "namespace": null,
236 | "name": null,
237 | "version": null,
238 | "qualifiers": null,
239 | "subpath": null,
240 | "is_invalid": true
241 | },
242 | {
243 | "description": "a namespace is required",
244 | "purl": "pkg:maven//io@1.3.4",
245 | "canonical_purl": "pkg:maven//io@1.3.4",
246 | "type": "maven",
247 | "namespace": null,
248 | "name": null,
249 | "version": null,
250 | "qualifiers": null,
251 | "subpath": null,
252 | "is_invalid": true
253 | },
254 | {
255 | "description": "slash / after scheme is not significant",
256 | "purl": "pkg:/maven/org.apache.commons/io",
257 | "canonical_purl": "pkg:maven/org.apache.commons/io",
258 | "type": "maven",
259 | "namespace": "org.apache.commons",
260 | "name": "io",
261 | "version": null,
262 | "qualifiers": null,
263 | "subpath": null,
264 | "is_invalid": false
265 | },
266 | {
267 | "description": "double slash // after scheme is not significant",
268 | "purl": "pkg://maven/org.apache.commons/io",
269 | "canonical_purl": "pkg:maven/org.apache.commons/io",
270 | "type": "maven",
271 | "namespace": "org.apache.commons",
272 | "name": "io",
273 | "version": null,
274 | "qualifiers": null,
275 | "subpath": null,
276 | "is_invalid": false
277 | },
278 | {
279 | "description": "slash /// after type is not significant",
280 | "purl": "pkg:///maven/org.apache.commons/io",
281 | "canonical_purl": "pkg:maven/org.apache.commons/io",
282 | "type": "maven",
283 | "namespace": "org.apache.commons",
284 | "name": "io",
285 | "version": null,
286 | "qualifiers": null,
287 | "subpath": null,
288 | "is_invalid": false
289 | },
290 | {
291 | "description": "valid maven purl",
292 | "purl": "pkg:maven/HTTPClient/HTTPClient@0.3-3",
293 | "canonical_purl": "pkg:maven/HTTPClient/HTTPClient@0.3-3",
294 | "type": "maven",
295 | "namespace": "HTTPClient",
296 | "name": "HTTPClient",
297 | "version": "0.3-3",
298 | "qualifiers": null,
299 | "subpath": null,
300 | "is_invalid": false
301 | },
302 | {
303 | "description": "valid maven purl containing a space in the version and qualifier",
304 | "purl": "pkg:maven/mygroup/myartifact@1.0.0%20Final?mykey=my%20value",
305 | "canonical_purl": "pkg:maven/mygroup/myartifact@1.0.0%20Final?mykey=my%20value",
306 | "type": "maven",
307 | "namespace": "mygroup",
308 | "name": "myartifact",
309 | "version": "1.0.0 Final",
310 | "qualifiers": {"mykey": "my value"},
311 | "subpath": null,
312 | "is_invalid": false
313 | },
314 | {
315 | "description": "valid debian purl containing a plus in the name and version",
316 | "purl": "pkg:deb/debian/g++-10@10.2.1+6",
317 | "canonical_purl": "pkg:deb/debian/g%2B%2B-10@10.2.1%2B6",
318 | "type": "deb",
319 | "namespace": "debian",
320 | "name": "g++-10",
321 | "version": "10.2.1+6",
322 | "qualifiers": null,
323 | "subpath": null,
324 | "is_invalid": false
325 | },
326 | {
327 | "description": "checks for invalid qualifier keys",
328 | "purl": "pkg:npm/myartifact@1.0.0?in%20production=true",
329 | "canonical_purl": null,
330 | "type": "npm",
331 | "namespace": null,
332 | "name": "myartifact",
333 | "version": "1.0.0",
334 | "qualifiers": {"in production": "true"},
335 | "subpath": null,
336 | "is_invalid": true
337 | },
338 | {
339 | "description": "valid hackage purl",
340 | "purl": "pkg:hackage/AC-HalfInteger@1.2.1",
341 | "canonical_purl": "pkg:hackage/AC-HalfInteger@1.2.1",
342 | "type": "hackage",
343 | "namespace": null,
344 | "name": "AC-HalfInteger",
345 | "version": "1.2.1",
346 | "qualifiers": null,
347 | "subpath": null,
348 | "is_invalid": false
349 | },
350 | {
351 | "description": "name and version are always required",
352 | "purl": "pkg:hackage",
353 | "canonical_purl": "pkg:hackage",
354 | "type": "hackage",
355 | "namespace": null,
356 | "name": null,
357 | "version": null,
358 | "qualifiers": null,
359 | "subpath": null,
360 | "is_invalid": true
361 | }
362 | ]
363 |
--------------------------------------------------------------------------------
/src/test/resources/type-namespace-constructor-only.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "description": "valid",
4 | "type": "validtype",
5 | "name": "name",
6 | "canonical_purl": "pkg:validtype/name",
7 | "is_invalid": false
8 | },
9 | {
10 | "description": "constructor with an empty type should have thrown an error",
11 | "type": "",
12 | "name": "name",
13 | "is_invalid": true
14 | },
15 | {
16 | "description": "constructor with `invalid^type` should have thrown an error",
17 | "type": "invalid^type",
18 | "name": "name",
19 | "is_invalid": true
20 | },
21 | {
22 | "description": "constructor with `0invalid` should have thrown an error",
23 | "type": "0invalid",
24 | "name": "name",
25 | "is_invalid": true
26 | }
27 | ]
28 |
--------------------------------------------------------------------------------