From 057b434f89842f648290f933d48b4a04ba360674 Mon Sep 17 00:00:00 2001 From: Matthew Donoughe Date: Mon, 10 Mar 2025 13:25:30 -0400 Subject: [PATCH 1/2] fix handling of "" "." ".." "%2E" etc in subpaths --- purl/src/builder.rs | 27 ++++++++++++++++++++++++++- purl/src/parse.rs | 16 +++++++++++++--- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/purl/src/builder.rs b/purl/src/builder.rs index 3e7c2ac..9199d88 100644 --- a/purl/src/builder.rs +++ b/purl/src/builder.rs @@ -171,7 +171,26 @@ impl GenericPurlBuilder { where SmallString: From, { - self.parts.subpath = SmallString::from(new); + let new = SmallString::from(new); + + // PURL subpaths are forbidden to contain these segments. + // The parsing spec says to remove them, so remove them here too. + let new = if new.split('/').any(|segment| ["", ".", ".."].contains(&segment)) { + let mut cleaned = SmallString::new(); + let mut segments = new.split('/').filter(|segment| !["", ".", ".."].contains(segment)); + if let Some(first) = segments.next() { + cleaned.push_str(first); + for rest in segments { + cleaned.push('/'); + cleaned.push_str(rest); + } + } + cleaned + } else { + new + }; + + self.parts.subpath = new; self } @@ -394,6 +413,12 @@ mod tests { assert_eq!("", &builder.parts.subpath); } + #[test] + fn with_subpath_some_normalizes_subpath() { + let builder = GenericPurlBuilder::<&str>::default().with_subpath("/.././/...//."); + assert_eq!("...", &builder.parts.subpath); + } + #[test] fn build_works() { let purl = GenericPurlBuilder::default() diff --git a/purl/src/parse.rs b/purl/src/parse.rs index 1d87e8e..a13f5bb 100644 --- a/purl/src/parse.rs +++ b/purl/src/parse.rs @@ -236,13 +236,16 @@ fn decode_subpath(subpath: &str) -> Result { let mut rebuilt = SmallString::new(); for segment in subpath.split('/') { - if ["", ".", ".."].contains(&segment) { + if segment.is_empty() { continue; } let decoded = decode(segment)?; - if decoded.contains('/') || [".", ".."].contains(&&*decoded) { + if decoded.contains('/') { return Err(ParseError::InvalidEscape); } + if decoded.len() < 3 && decoded.chars().all(|c| c == '.') { + continue; + } if !rebuilt.is_empty() { rebuilt.push('/'); } @@ -396,9 +399,16 @@ mod tests { assert_eq!("pkg:type/a/b/./c/../d/name", &parsed.to_string()); } + #[test] + fn parse_when_subpath_contains_weird_components_preserves_them() { + let parsed = GenericPurl::::from_str("pkg:type/name#a/.../b/").unwrap(); + assert_eq!("pkg:type/name#a/.../b", &parsed.to_string()); + } + #[test] fn parse_when_subpath_contains_invalid_components_skips_them() { - let parsed = GenericPurl::::from_str("pkg:type/name#/a//b/./c/../d/").unwrap(); + let parsed = + GenericPurl::::from_str("pkg:type/name#/a//b/./c/../%2E%2E/d/").unwrap(); assert_eq!("pkg:type/name#a/b/c/d", &parsed.to_string()); } From 12a07bb6472477f997fae8de91bdf3a372720892 Mon Sep 17 00:00:00 2001 From: Matthew Donoughe Date: Mon, 10 Mar 2025 13:39:31 -0400 Subject: [PATCH 2/2] disallow package types starting with digits --- purl/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/purl/src/lib.rs b/purl/src/lib.rs index 4a90591..a14f8d2 100644 --- a/purl/src/lib.rs +++ b/purl/src/lib.rs @@ -381,8 +381,10 @@ fn is_valid_package_type(package_type: &str) -> bool { // https://github.com/package-url/purl-spec/blob/master/PURL-SPECIFICATION.rst#rules-for-each-purl-component const ALLOWED_SPECIAL_CHARS: &[char] = &['.', '+', '-']; !package_type.is_empty() + && package_type.starts_with(|c: char| c.is_ascii_alphabetic()) && package_type .chars() + .skip(1) .all(|c| c.is_ascii_alphanumeric() || ALLOWED_SPECIAL_CHARS.contains(&c)) }