diff --git a/purl/src/format.rs b/purl/src/format.rs index 96fabd4..c6c1bce 100644 --- a/purl/src/format.rs +++ b/purl/src/format.rs @@ -17,13 +17,14 @@ const PATH: &AsciiSet = &QUERY.add(b'?').add(b'`').add(b'{').add(b'}'); // https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst#how-to-build-purl-string-from-its-components // We mostly use the standard URL rules, but the PURL spec says '@' '?' '#' must -// be escaped except when used as a separator. -const PURL_PATH: &AsciiSet = &PATH.add(b'@').add(b'?').add(b'#'); +// be escaped except when used as a separator, and we do all the encoding in one +// pass so we need to include '%'. +const PURL_PATH: &AsciiSet = &PATH.add(b'@').add(b'?').add(b'#').add(b'%'); const PURL_PATH_SEGMENT: &AsciiSet = &PURL_PATH.add(b'/'); // For compatibility with PURL implementations that treat qualifiers as // form-urlencoded, escape '+' as well. -const PURL_QUERY: &AsciiSet = &QUERY.add(b'@').add(b'?').add(b'#').add(b'+'); -const PURL_FRAGMENT: &AsciiSet = &FRAGMENT.add(b'@').add(b'?').add(b'#'); +const PURL_QUERY: &AsciiSet = &QUERY.add(b'@').add(b'?').add(b'#').add(b'+').add(b'%'); +const PURL_FRAGMENT: &AsciiSet = &FRAGMENT.add(b'@').add(b'?').add(b'#').add(b'%'); impl fmt::Display for GenericPurl where diff --git a/purl_test/src/lib.rs b/purl_test/src/lib.rs index cc228a6..99847b1 100644 --- a/purl_test/src/lib.rs +++ b/purl_test/src/lib.rs @@ -1260,3 +1260,39 @@ fn plus_signs_and_spaces() { "Incorrect string representation" ); } +#[test] +/// unsupported: percent signs +fn unsupported_percent_signs() { + assert!( + matches!(Purl::from_str("pkg:generic/%40100%25/100%25@100%25?repository_url=https://example.com/100%2525/#100%25"), + Err(PackageError::UnsupportedType)), "Type {} is not supported", "generic" + ); + let parsed = match GenericPurl::::from_str( + "pkg:generic/%40100%25/100%25@100%25?repository_url=https://example.com/100%2525/#100%25", + ) { + Ok(purl) => purl, + Err(error) => { + panic!( + "Failed to parse valid purl {:?}: {}", + "pkg:generic/%40100%25/100%25@100%25?repository_url=https://example.com/100%2525/#100%25", + error + ) + }, + }; + assert_eq!("generic", parsed.package_type(), "Incorrect package type"); + assert_eq!(Some("@100%"), parsed.namespace(), "Incorrect namespace"); + assert_eq!("100%", parsed.name(), "Incorrect name"); + assert_eq!(Some("100%"), parsed.version(), "Incorrect version"); + assert_eq!(Some("100%"), parsed.subpath(), "Incorrect subpath"); + let expected_qualifiers: HashMap<&str, &str> = + [("repository_url", "https://example.com/100%25/")].into_iter().collect(); + assert_eq!( + expected_qualifiers, + parsed.qualifiers().iter().map(|(k, v)| (k.as_str(), v)).collect::>() + ); + assert_eq!( + "pkg:generic/%40100%25/100%25@100%25?repository_url=https://example.com/100%2525/#100%25", + &parsed.to_string(), + "Incorrect string representation" + ); +} diff --git a/xtask/src/generate_tests/phylum-test-suite-data.json b/xtask/src/generate_tests/phylum-test-suite-data.json index 19a4896..2e931a0 100644 --- a/xtask/src/generate_tests/phylum-test-suite-data.json +++ b/xtask/src/generate_tests/phylum-test-suite-data.json @@ -120,5 +120,19 @@ }, "subpath": null, "is_invalid": false + }, + { + "description": "percent signs", + "purl": "pkg:generic/%40100%25/100%25@100%25?repository_url=https://example.com/100%2525/#100%25", + "canonical_purl": "pkg:generic/%40100%25/100%25@100%25?repository_url=https://example.com/100%2525/#100%25", + "type": "generic", + "namespace": "@100%", + "name": "100%", + "version": "100%", + "qualifiers": { + "repository_url": "https://example.com/100%25/" + }, + "subpath": "100%", + "is_invalid": false } ]