Skip to content

Commit 7aa82fa

Browse files
authored
better align parsing with spec (#17)
1 parent 3d21fd1 commit 7aa82fa

File tree

5 files changed

+60
-26
lines changed

5 files changed

+60
-26
lines changed

purl/src/format.rs

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ where
5656

5757
if let Some(version) = self.version() {
5858
// The version is a continuation of the same path segment.
59-
write!(f, "@{}", utf8_percent_encode(version, PURL_PATH_SEGMENT))?;
59+
write!(f, "@{}", utf8_percent_encode(version, PURL_PATH))?;
6060
}
6161

6262
if !self.parts.qualifiers.is_empty() {
@@ -162,18 +162,6 @@ mod tests {
162162
);
163163
}
164164

165-
#[test]
166-
fn display_encodes_version_correctly() {
167-
assert_eq!(
168-
"pkg:generic/name@a%23%2Fb%3F%2Fc%40",
169-
&GenericPurlBuilder::new(Cow::Borrowed("generic"), "name")
170-
.with_version("a#/b?/c@")
171-
.build()
172-
.expect("Could not build PURL")
173-
.to_string(),
174-
);
175-
}
176-
177165
#[test]
178166
fn display_encodes_qualifiers_correctly() {
179167
assert_eq!(

purl/src/lib.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
use std::borrow::Cow;
55

66
pub use builder::*;
7-
pub use format::*;
87
#[cfg(feature = "package-type")]
98
pub use package_type::*;
109
pub use parse::*;

purl/src/parse.rs

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ where
167167
fn from_str(s: &str) -> Result<Self, Self::Err> {
168168
// This mostly follows the procedure documented in the PURL spec.
169169
// https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst#how-to-parse-a-purl-string-in-its-components
170+
171+
// Check for `pkg:` first to quickly reject non-PURLs.
170172
let s = s.strip_prefix("pkg:").ok_or(ParseError::UnsupportedUrlScheme)?;
171173

172174
// PURLs are not supposed to have any leading slashes, but the spec says that
@@ -177,15 +179,15 @@ where
177179

178180
// Remove subpath and qualifiers from the end now because they have higher
179181
// precedence than the path separater.
180-
let s = match s.split_once('#') {
182+
let s = match s.rsplit_once('#') {
181183
Some((s, subpath)) => {
182184
parts.subpath = decode_subpath(subpath)?;
183185
s
184186
},
185187
None => s,
186188
};
187189

188-
let s = match s.split_once('?') {
190+
let s = match s.rsplit_once('?') {
189191
Some((s, qualifiers)) => {
190192
decode_qualifiers(qualifiers, &mut parts)?;
191193
s
@@ -206,24 +208,24 @@ where
206208

207209
let package_type = T::from_str(package_type)?;
208210

211+
let s = match s.rsplit_once('@') {
212+
Some((s, version)) => {
213+
parts.version = decode(version)?.into();
214+
s
215+
},
216+
None => s,
217+
};
218+
209219
// The namespace is optional so we may not have any more slashes.
210-
let name_and_version = match s.rsplit_once('/') {
220+
let name = match s.rsplit_once('/') {
211221
Some((namespace, s)) => {
212222
parts.namespace = decode_namespace(namespace)?;
213223
s
214224
},
215225
None => s,
216226
};
217227

218-
match name_and_version.rsplit_once('@') {
219-
Some((name, version)) => {
220-
parts.name = decode(name)?.into();
221-
parts.version = decode(version)?.into();
222-
},
223-
None => {
224-
parts.name = decode(name_and_version)?.into();
225-
},
226-
};
228+
parts.name = decode(name)?.into();
227229

228230
GenericPurlBuilder { package_type, parts }.build()
229231
}

purl_test/src/lib.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1296,3 +1296,39 @@ fn unsupported_percent_signs_are_properly_encoded_and_decoded() {
12961296
"Incorrect string representation"
12971297
);
12981298
}
1299+
#[test]
1300+
/// unsupported: version encoding
1301+
fn unsupported_version_encoding() {
1302+
assert!(
1303+
matches!(
1304+
Purl::from_str("pkg:generic/name@a%23%2Fb%3F%2Fc%40"),
1305+
Err(PackageError::UnsupportedType)
1306+
),
1307+
"Type {} is not supported",
1308+
"generic"
1309+
);
1310+
let parsed = match GenericPurl::<String>::from_str("pkg:generic/name@a%23%2Fb%3F%2Fc%40") {
1311+
Ok(purl) => purl,
1312+
Err(error) => {
1313+
panic!(
1314+
"Failed to parse valid purl {:?}: {}",
1315+
"pkg:generic/name@a%23%2Fb%3F%2Fc%40", error
1316+
)
1317+
},
1318+
};
1319+
assert_eq!("generic", parsed.package_type(), "Incorrect package type");
1320+
assert_eq!(None, parsed.namespace(), "Incorrect namespace");
1321+
assert_eq!("name", parsed.name(), "Incorrect name");
1322+
assert_eq!(Some("a#/b?/c@"), parsed.version(), "Incorrect version");
1323+
assert_eq!(None, parsed.subpath(), "Incorrect subpath");
1324+
let expected_qualifiers: HashMap<&str, &str> = HashMap::new();
1325+
assert_eq!(
1326+
expected_qualifiers,
1327+
parsed.qualifiers().iter().map(|(k, v)| (k.as_str(), v)).collect::<HashMap<&str, &str>>()
1328+
);
1329+
assert_eq!(
1330+
"pkg:generic/name@a%23/b%3F/c%40",
1331+
&parsed.to_string(),
1332+
"Incorrect string representation"
1333+
);
1334+
}

xtask/src/generate_tests/phylum-test-suite-data.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,5 +134,14 @@
134134
},
135135
"subpath": "100%",
136136
"is_invalid": false
137+
},
138+
{
139+
"description": "version encoding",
140+
"purl": "pkg:generic/name@a%23%2Fb%3F%2Fc%40",
141+
"canonical_purl": "pkg:generic/name@a%23/b%3F/c%40",
142+
"type": "generic",
143+
"name": "name",
144+
"version": "a#/b?/c@",
145+
"is_invalid": false
137146
}
138147
]

0 commit comments

Comments
 (0)