Skip to content

Commit 3c14187

Browse files
committed
fix: make fields final
`PackageURL` is an immutable class, so the fields should be `final`. This is achieved by moving the parsing code into the constructor. This fixes `@NullMarked` fields must be initialized.
1 parent f3762e6 commit 3c14187

File tree

1 file changed

+90
-93
lines changed

1 file changed

+90
-93
lines changed

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

Lines changed: 90 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -74,37 +74,37 @@ public final class PackageURL implements Serializable {
7474
* The package "type" or package "protocol" such as maven, npm, nuget, gem, pypi, etc.
7575
* Required.
7676
*/
77-
private String type;
77+
private final String type;
7878

7979
/**
8080
* The name prefix such as a Maven groupid, a Docker image owner, a GitHub user or organization.
8181
* Optional and type-specific.
8282
*/
83-
private @Nullable String namespace;
83+
private final @Nullable String namespace;
8484

8585
/**
8686
* The name of the package.
8787
* Required.
8888
*/
89-
private String name;
89+
private final String name;
9090

9191
/**
9292
* The version of the package.
9393
* Optional.
9494
*/
95-
private @Nullable String version;
95+
private final @Nullable String version;
9696

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

103103
/**
104104
* Extra subpath within a package, relative to the package root.
105105
* Optional.
106106
*/
107-
private @Nullable String subpath;
107+
private final @Nullable String subpath;
108108

109109
/**
110110
* Constructs a new PackageURL object by parsing the specified string.
@@ -114,7 +114,90 @@ public final class PackageURL implements Serializable {
114114
* @throws NullPointerException if {@code purl} is {@code null}
115115
*/
116116
public PackageURL(final String purl) throws MalformedPackageURLException {
117-
parse(requireNonNull(purl, "purl"));
117+
requireNonNull(purl, "purl");
118+
119+
if (purl.isEmpty()) {
120+
throw new MalformedPackageURLException("Invalid purl: Is empty or null");
121+
}
122+
123+
try {
124+
if (!purl.startsWith(SCHEME_PART)) {
125+
throw new MalformedPackageURLException(
126+
"Invalid purl: " + purl + ". It does not start with '" + SCHEME_PART + "'");
127+
}
128+
129+
final int length = purl.length();
130+
int start = SCHEME_PART.length();
131+
132+
while (start < length && '/' == purl.charAt(start)) {
133+
start++;
134+
}
135+
136+
final URI uri = new URI(String.join("/", SCHEME_PART, purl.substring(start)));
137+
138+
validateScheme(uri.getScheme());
139+
140+
// Check to ensure that none of these parts are parsed. If so, it's an invalid purl.
141+
if (uri.getRawAuthority() != null) {
142+
throw new MalformedPackageURLException("Invalid purl: A purl must NOT contain a URL Authority ");
143+
}
144+
145+
// subpath is optional - check for existence
146+
final String rawFragment = uri.getRawFragment();
147+
if (rawFragment != null && !rawFragment.isEmpty()) {
148+
this.subpath = validatePath(parsePath(rawFragment, true), true);
149+
} else {
150+
this.subpath = null;
151+
}
152+
// qualifiers are optional - check for existence
153+
final String rawQuery = uri.getRawQuery();
154+
if (rawQuery != null && !rawQuery.isEmpty()) {
155+
this.qualifiers = parseQualifiers(rawQuery);
156+
} else {
157+
this.qualifiers = null;
158+
}
159+
// this is the rest of the purl that needs to be parsed
160+
String remainder = uri.getRawPath();
161+
// trim trailing '/'
162+
int end = remainder.length() - 1;
163+
while (end > 0 && '/' == remainder.charAt(end)) {
164+
end--;
165+
}
166+
remainder = remainder.substring(0, end + 1);
167+
// there is exactly one leading '/' at this point
168+
start = 1;
169+
// type
170+
int index = remainder.indexOf('/', start);
171+
if (index <= start) {
172+
throw new MalformedPackageURLException("Invalid purl: does not contain both a type and name");
173+
}
174+
this.type = toLowerCase(validateType(remainder.substring(start, index)));
175+
176+
start = index + 1;
177+
178+
// version is optional - check for existence
179+
index = remainder.lastIndexOf('@');
180+
if (index >= start) {
181+
this.version = validateVersion(this.type, percentDecode(remainder.substring(index + 1)));
182+
remainder = remainder.substring(0, index);
183+
} else {
184+
this.version = null;
185+
}
186+
187+
// The 'remainder' should now consist of an optional namespace and the name
188+
index = remainder.lastIndexOf('/');
189+
if (index <= start) {
190+
this.name = validateName(this.type, percentDecode(remainder.substring(start)));
191+
this.namespace = null;
192+
} else {
193+
this.name = validateName(this.type, percentDecode(remainder.substring(index + 1)));
194+
remainder = remainder.substring(0, index);
195+
this.namespace = validateNamespace(this.type, parsePath(remainder.substring(start), false));
196+
}
197+
verifyTypeConstraints(this.type, this.namespace, this.name);
198+
} catch (URISyntaxException e) {
199+
throw new MalformedPackageURLException("Invalid purl: " + e.getMessage(), e);
200+
}
118201
}
119202

120203
/**
@@ -685,92 +768,6 @@ static String percentEncode(final String source) {
685768
return changed ? new String(buffer.array(), 0, buffer.position(), StandardCharsets.UTF_8) : source;
686769
}
687770

688-
/**
689-
* Given a specified PackageURL, this method will parse the purl and populate this classes
690-
* instance fields so that the corresponding getters may be called to retrieve the individual
691-
* pieces of the purl.
692-
*
693-
* @param purl the purl string to parse
694-
* @throws MalformedPackageURLException if an exception occurs when parsing
695-
*/
696-
private void parse(final String purl) throws MalformedPackageURLException {
697-
if (purl.isEmpty()) {
698-
throw new MalformedPackageURLException("Invalid purl: Is empty or null");
699-
}
700-
701-
try {
702-
if (!purl.startsWith(SCHEME_PART)) {
703-
throw new MalformedPackageURLException(
704-
"Invalid purl: " + purl + ". It does not start with '" + SCHEME_PART + "'");
705-
}
706-
707-
final int length = purl.length();
708-
int start = SCHEME_PART.length();
709-
710-
while (start < length && '/' == purl.charAt(start)) {
711-
start++;
712-
}
713-
714-
final URI uri = new URI(String.join("/", SCHEME_PART, purl.substring(start)));
715-
716-
validateScheme(uri.getScheme());
717-
718-
// Check to ensure that none of these parts are parsed. If so, it's an invalid purl.
719-
if (uri.getRawAuthority() != null) {
720-
throw new MalformedPackageURLException("Invalid purl: A purl must NOT contain a URL Authority ");
721-
}
722-
723-
// subpath is optional - check for existence
724-
final String rawFragment = uri.getRawFragment();
725-
if (rawFragment != null && !rawFragment.isEmpty()) {
726-
this.subpath = validatePath(parsePath(rawFragment, true), true);
727-
}
728-
// qualifiers are optional - check for existence
729-
final String rawQuery = uri.getRawQuery();
730-
if (rawQuery != null && !rawQuery.isEmpty()) {
731-
this.qualifiers = parseQualifiers(rawQuery);
732-
}
733-
// this is the rest of the purl that needs to be parsed
734-
String remainder = uri.getRawPath();
735-
// trim trailing '/'
736-
int end = remainder.length() - 1;
737-
while (end > 0 && '/' == remainder.charAt(end)) {
738-
end--;
739-
}
740-
remainder = remainder.substring(0, end + 1);
741-
// there is exactly one leading '/' at this point
742-
start = 1;
743-
// type
744-
int index = remainder.indexOf('/', start);
745-
if (index <= start) {
746-
throw new MalformedPackageURLException("Invalid purl: does not contain both a type and name");
747-
}
748-
this.type = toLowerCase(validateType(remainder.substring(start, index)));
749-
750-
start = index + 1;
751-
752-
// version is optional - check for existence
753-
index = remainder.lastIndexOf('@');
754-
if (index >= start) {
755-
this.version = validateVersion(this.type, percentDecode(remainder.substring(index + 1)));
756-
remainder = remainder.substring(0, index);
757-
}
758-
759-
// The 'remainder' should now consist of an optional namespace and the name
760-
index = remainder.lastIndexOf('/');
761-
if (index <= start) {
762-
this.name = validateName(this.type, percentDecode(remainder.substring(start)));
763-
} else {
764-
this.name = validateName(this.type, percentDecode(remainder.substring(index + 1)));
765-
remainder = remainder.substring(0, index);
766-
this.namespace = validateNamespace(this.type, parsePath(remainder.substring(start), false));
767-
}
768-
verifyTypeConstraints(this.type, this.namespace, this.name);
769-
} catch (URISyntaxException e) {
770-
throw new MalformedPackageURLException("Invalid purl: " + e.getMessage(), e);
771-
}
772-
}
773-
774771
/**
775772
* Some purl types may have specific constraints. This method attempts to verify them.
776773
* @param type the purl type

0 commit comments

Comments
 (0)