Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/parsers/podfile_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,11 @@ fn parse_dep_to_base_purl_and_version(dep: &str) -> (String, Option<String>) {
(base_purl, requirement)
}

/// The internal key a pod is tracked by while parsing — never emitted.
///
/// Emitted PURLs come from `create_cocoapods_purl`, which encodes through the
/// crate. This only has to be a stable key for the maps below, so it is not
/// encoded and must not start being used as an output value.
fn make_base_purl(name: &str) -> String {
format!("pkg:cocoapods/{}", name)
}
Expand Down
24 changes: 9 additions & 15 deletions src/parsers/poetry_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ fn normalize_pypi_name(name: &str) -> String {

fn create_pypi_purl(name: &str, version: Option<&str>) -> Option<String> {
if name.contains('[') || name.contains(']') {
return Some(truncate_field(build_manual_pypi_purl(name, version)));
return build_manual_pypi_purl(name, version).map(truncate_field);
}

let mut purl = PackageUrl::new(PoetryLockParser::PACKAGE_TYPE.as_str(), name).ok()?;
Expand All @@ -414,20 +414,14 @@ fn create_pypi_purl(name: &str, version: Option<&str>) -> Option<String> {
Some(truncate_field(purl.to_string()))
}

fn build_manual_pypi_purl(name: &str, version: Option<&str>) -> String {
let encoded_name = encode_pypi_name(name);
let mut purl = format!("pkg:pypi/{}", encoded_name);
if let Some(version) = version
&& !version.is_empty()
{
purl.push('@');
purl.push_str(version);
}
purl
}

fn encode_pypi_name(name: &str) -> String {
name.replace('[', "%5b").replace(']', "%5d")
/// Builds a PyPI PURL for a name carrying extras (`requests[socks]`).
///
/// The crate encodes brackets as `%5B`/`%5D`; this lowercases those two escapes
/// to keep the spelling ScanCode emits. Everything else still goes through the
/// encoder, so a name that also contains a reserved character is not left raw.
fn build_manual_pypi_purl(name: &str, version: Option<&str>) -> Option<String> {
crate::parsers::utils::simple_purl("pypi", name, version)
.map(|purl| purl.replace("%5B", "%5b").replace("%5D", "%5d"))
}

fn extract_sha256_from_files(package_table: &TomlMap<String, TomlValue>) -> Option<String> {
Expand Down
3 changes: 3 additions & 0 deletions src/parsers/python/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,9 @@ pub(super) fn build_python_dependency_purl(name: &str, version: Option<&str>) ->
return None;
}

// Safe to assemble by hand: the validation above restricts the name to the
// PEP 508 charset, which needs no percent-encoding, and the version is
// encoded by `encode_python_dependency_purl_version`.
Some(match version {
Some(version) => format!(
"pkg:pypi/{normalized_name}@{}",
Expand Down
3 changes: 3 additions & 0 deletions src/parsers/requirements_txt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -938,6 +938,9 @@ fn create_pypi_purl(name: &str, version: Option<&str>) -> Option<String> {
return None;
}

// Safe to assemble by hand: `is_valid_distribution_name` above restricts the
// name to the PEP 508 charset, which needs no percent-encoding, and the
// version is encoded by `encode_pypi_purl_version`.
PackageUrl::new(RequirementsTxtParser::PACKAGE_TYPE.as_str(), name)
.ok()
.map(|_| match version {
Expand Down
20 changes: 9 additions & 11 deletions src/parsers/uv_lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -932,7 +932,7 @@ fn normalize_pypi_name(name: &str) -> String {

fn create_pypi_purl(name: &str, version: Option<&str>) -> Option<String> {
if name.contains('[') || name.contains(']') {
return Some(truncate_field(build_manual_pypi_purl(name, version)));
return build_manual_pypi_purl(name, version).map(truncate_field);
}

let mut purl = PackageUrl::new(UvLockParser::PACKAGE_TYPE.as_str(), name).ok()?;
Expand All @@ -942,16 +942,14 @@ fn create_pypi_purl(name: &str, version: Option<&str>) -> Option<String> {
Some(truncate_field(purl.to_string()))
}

fn build_manual_pypi_purl(name: &str, version: Option<&str>) -> String {
let encoded_name = name.replace('[', "%5b").replace(']', "%5d");
let mut purl = format!("pkg:pypi/{}", encoded_name);
if let Some(version) = version
&& !version.is_empty()
{
purl.push('@');
purl.push_str(version);
}
purl
/// Builds a PyPI PURL for a name carrying extras (`requests[socks]`).
///
/// The crate encodes brackets as `%5B`/`%5D`; this lowercases those two escapes
/// to keep the spelling ScanCode emits. Everything else still goes through the
/// encoder, so a name that also contains a reserved character is not left raw.
fn build_manual_pypi_purl(name: &str, version: Option<&str>) -> Option<String> {
crate::parsers::utils::simple_purl("pypi", name, version)
.map(|purl| purl.replace("%5B", "%5b").replace("%5D", "%5d"))
}

fn toml_value_to_json(value: &TomlValue) -> JsonValue {
Expand Down
Loading