Parse a purl from right to left. Split once on the hash for the subpath, then on the question mark for qualifiers, then take the scheme and type from the left, the version after the last at sign, the name after the last slash, and the rest as namespace. Percent decode each part. Do not split on slashes first.
Why order matters
A purl looks like a URL, so developers reach for a URL parser or a regular expression. That goes wrong because the same characters mean different things at different positions. A slash separates namespace segments in one place and subpath segments in another. A plus sign can appear in a Debian version, written in encoded form.
The package URL specification documents the parsing order so every library gets the same result. This post follows its how to parse page.
The steps
The specification says to work from right to left, from subpath to type.
- Split the whole string once from the right on the hash. The right side is the subpath. Split it on slashes, percent decode each segment, discard segments that are empty, a single dot or a double dot, and report an error if a segment contains a slash. The left side is the remainder.
- Split the remainder once from the right on the question mark. The right side is the qualifiers string. Split it on the ampersand. For each pair, split once from the left on the equals sign. The key is the lowercase left side. The value is the percent decoded right side. Discard pairs with an empty value. If the key is checksum, split its value on commas.
- Split the remainder once from the left on the colon. The left side, lowercased, is the scheme.
- Strip all leading slashes from the remainder, then split once from the left on a slash. The left side, lowercased, is the type.
- Split the remainder once from the right on the at sign. The right side, percent decoded, is the version.
- Strip trailing slashes, then split once from the right on a slash. The right side, percent decoded, is the name. Apply type specific normalization to it if needed.
- Whatever remains is the namespace. Split on slashes, drop empty segments, percent decode each, apply type normalization, and join with a slash.
A worked example
Take this purl, which is a Debian example from the specification.
pkg:deb/debian/attr@1:2.4.47-2%2Bb1?arch=amd64
There is no hash, so the subpath is empty. The qualifiers string is arch=amd64. The scheme is pkg and the type is deb. The version is 1:2.4.47-2%2Bb1, which decodes to 1:2.4.47-2+b1. The name is attr and the namespace is debian.
Note the encoded plus sign. If you decoded before splitting, or skipped decoding, you would store the wrong version and fail to match advisories.
Traps to test for
| Input feature | What goes wrong |
|---|---|
| Trailing slashes before the name | Spec says strip them |
| Encoded characters in version | Matching fails if you compare the raw text |
| Empty qualifier values | Spec says discard them |
| Uppercase qualifier keys | Spec says keys are lowercased |
| Extra leading slashes after the scheme | Spec says strip them |
Test against the official cases
The purl project keeps a shared test suite in its repository. Run your parser against it instead of writing your own examples, since the suite covers type specific cases that are easy to forget.
Try it yourself
Click through it at your own pace, or open it full screen. Notice how each component has one identifier that downstream lookups depend on.
Where this fits
A wrong parse silently breaks vulnerability matching. For writing purls, see how to write a correct purl for npm and Maven. Safeguard offers SBOM and dependency scanning, and the free scan lets you see component identifiers on a real project.
Quick check
Should I use a generic URL parser for a purl?
No. A purl has its own rules for where each part ends, so use a purl library or follow the specification's right to left order.
Are qualifier keys case sensitive?
The parsing steps lowercase the key, so treat keys as lowercase.
Sources
Last reviewed: 19 October 2026.