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
71 changes: 70 additions & 1 deletion cargo-cyclonedx/src/purl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,51 @@ pub fn get_purl(
}

/// Converts the `cargo metadata`'s `source` field to a valid PURL `vcs_url`.
///
/// The `vcs_url` qualifier is specified to use the SPDX Package Download Location format:
/// `<vcs_tool>+<transport>://<host_name>[/<path_to_repository>][@<revision_tag_or_branch>][#<sub_path>]`
///
/// Cargo metadata uses a different format:
/// `git+<url>[?branch=<branch>|?tag=<tag>|?rev=<rev>]#<commit_hash>`
///
/// This function strips the query parameters (since the commit hash already identifies the code)
/// and converts the `#commit_hash` to `@commit_hash` per the SPDX format.
///
/// Assumes that the source kind is `git`, panics if it isn't.
fn source_to_vcs_url(source: &cargo_metadata::Source) -> String {
assert!(source.repr.starts_with("git+"));
source.repr.replace('#', "@")
let url = &source.repr;
// Find where query parameters start (if any) and where the commit hash fragment starts
let query_start = url.find('?');
let fragment_start = url.find('#');
match (query_start, fragment_start) {
// Has both query params and commit hash: strip query, keep commit as @
(Some(q), Some(f)) => {
let base = &url[..q];
let commit = &url[f + 1..];
format!("{}@{}", base, commit)
}
// No query params, has commit hash: just replace # with @
(None, Some(_)) => url.replace('#', "@"),
// Has query params but no commit hash: extract the ref value as @
(Some(q), None) => {
let base = &url[..q];
let query = &url[q + 1..];
// Extract the value from branch=X, tag=X, or rev=X
let ref_value = query
.split('&')
.find_map(|param| {
param
.strip_prefix("branch=")
.or_else(|| param.strip_prefix("tag="))
.or_else(|| param.strip_prefix("rev="))
})
.unwrap_or(query);
format!("{}@{}", base, ref_value)
}
// No query params, no commit hash: return as-is
(None, None) => url.to_string(),
}
}

/// Converts a relative path to PURL subpath
Expand All @@ -91,6 +132,8 @@ mod tests {

const CRATES_IO_PACKAGE_JSON: &str = include_str!("../tests/fixtures/crates_io_package.json");
const GIT_PACKAGE_JSON: &str = include_str!("../tests/fixtures/git_package.json");
const GIT_PACKAGE_WITH_BRANCH_JSON: &str =
include_str!("../tests/fixtures/git_package_with_branch.json");
const ROOT_PACKAGE_JSON: &str = include_str!("../tests/fixtures/root_package.json");
const WORKSPACE_PACKAGE_JSON: &str = include_str!("../tests/fixtures/workspace_package.json");

Expand Down Expand Up @@ -129,6 +172,32 @@ mod tests {
assert!(parsed_purl.namespace().is_none());
}

#[test]
fn git_purl_with_branch() {
let git_package: Package = serde_json::from_str(GIT_PACKAGE_WITH_BRANCH_JSON).unwrap();
let purl = get_purl(&git_package, &git_package, Utf8Path::new("/foo/bar"), None).unwrap();
// Validate that data roundtripped correctly
let parsed_purl = Purl::from_str(purl.as_ref()).unwrap();
assert_eq!(parsed_purl.name(), "rav1d");
assert_eq!(parsed_purl.version(), Some("1.1.0"));
assert_eq!(parsed_purl.qualifiers().len(), 1);
let (qualifier, value) = parsed_purl.qualifiers().iter().next().unwrap();
assert_eq!(qualifier.as_str(), "vcs_url");
// The ?branch= query param must be stripped; only the commit hash remains after @
let decoded_value = percent_decode(value.as_bytes())
.decode_utf8()
.unwrap()
.to_string();
assert_eq!(
decoded_value,
"git+https://github.com/leo030303/rav1d.git@3a50834ce3743bc580f340ba3bfbdbf6a46ab783"
);
// Ensure ?branch= is NOT present in the vcs_url
assert!(!decoded_value.contains("?branch="));
assert!(parsed_purl.subpath().is_none());
assert!(parsed_purl.namespace().is_none());
}

#[test]
fn toplevel_package_purl() {
let root_package: Package = serde_json::from_str(ROOT_PACKAGE_JSON).unwrap();
Expand Down
270 changes: 270 additions & 0 deletions cargo-cyclonedx/tests/fixtures/git_package_with_branch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
{
"name": "rav1d",
"version": "1.1.0",
"id": "git+https://github.com/leo030303/rav1d.git?branch=add-rust-api#rav1d@1.1.0",
"license": "BSD-2-Clause",
"license_file": null,
"description": "Rust port of the dav1d AV1 decoder",
"source": "git+https://github.com/leo030303/rav1d.git?branch=add-rust-api#3a50834ce3743bc580f340ba3bfbdbf6a46ab783",
"dependencies": [
{
"name": "assert_matches",
"source": "registry+https://github.com/rust-lang/crates.io-index",
"req": "^1.5.0",
"kind": null,
"rename": null,
"optional": false,
"uses_default_features": true,
"features": [],
"target": null,
"registry": null
},
{
"name": "atomig",
"source": "registry+https://github.com/rust-lang/crates.io-index",
"req": "^0.4.0",
"kind": null,
"rename": null,
"optional": false,
"uses_default_features": true,
"features": [
"derive"
],
"target": null,
"registry": null
},
{
"name": "av-data",
"source": "registry+https://github.com/rust-lang/crates.io-index",
"req": "^0.4.2",
"kind": null,
"rename": null,
"optional": false,
"uses_default_features": true,
"features": [],
"target": null,
"registry": null
},
{
"name": "bitflags",
"source": "registry+https://github.com/rust-lang/crates.io-index",
"req": "^2.4.0",
"kind": null,
"rename": null,
"optional": false,
"uses_default_features": true,
"features": [],
"target": null,
"registry": null
},
{
"name": "cfg-if",
"source": "registry+https://github.com/rust-lang/crates.io-index",
"req": "^1.0.0",
"kind": null,
"rename": null,
"optional": false,
"uses_default_features": true,
"features": [],
"target": null,
"registry": null
},
{
"name": "libc",
"source": "registry+https://github.com/rust-lang/crates.io-index",
"req": "^0.2",
"kind": null,
"rename": null,
"optional": false,
"uses_default_features": true,
"features": [],
"target": null,
"registry": null
},
{
"name": "parking_lot",
"source": "registry+https://github.com/rust-lang/crates.io-index",
"req": "^0.12.2",
"kind": null,
"rename": null,
"optional": false,
"uses_default_features": true,
"features": [],
"target": null,
"registry": null
},
{
"name": "paste",
"source": "registry+https://github.com/rust-lang/crates.io-index",
"req": "^1.0.14",
"kind": null,
"rename": null,
"optional": false,
"uses_default_features": true,
"features": [],
"target": null,
"registry": null
},
{
"name": "raw-cpuid",
"source": "registry+https://github.com/rust-lang/crates.io-index",
"req": "^11.0.1",
"kind": null,
"rename": null,
"optional": false,
"uses_default_features": true,
"features": [],
"target": null,
"registry": null
},
{
"name": "static_assertions",
"source": "registry+https://github.com/rust-lang/crates.io-index",
"req": "^1.1.0",
"kind": null,
"rename": null,
"optional": false,
"uses_default_features": true,
"features": [],
"target": null,
"registry": null
},
{
"name": "strum",
"source": "registry+https://github.com/rust-lang/crates.io-index",
"req": "^0.27",
"kind": null,
"rename": null,
"optional": false,
"uses_default_features": true,
"features": [
"derive"
],
"target": null,
"registry": null
},
{
"name": "to_method",
"source": "registry+https://github.com/rust-lang/crates.io-index",
"req": "^1.1.0",
"kind": null,
"rename": null,
"optional": false,
"uses_default_features": true,
"features": [],
"target": null,
"registry": null
},
{
"name": "zerocopy",
"source": "registry+https://github.com/rust-lang/crates.io-index",
"req": "^0.7.32",
"kind": null,
"rename": null,
"optional": false,
"uses_default_features": true,
"features": [
"derive"
],
"target": null,
"registry": null
},
{
"name": "cc",
"source": "registry+https://github.com/rust-lang/crates.io-index",
"req": "^1.0.79",
"kind": "build",
"rename": null,
"optional": false,
"uses_default_features": true,
"features": [],
"target": null,
"registry": null
},
{
"name": "nasm-rs",
"source": "registry+https://github.com/rust-lang/crates.io-index",
"req": "^0.3",
"kind": "build",
"rename": null,
"optional": false,
"uses_default_features": true,
"features": [
"parallel"
],
"target": null,
"registry": null
}
],
"targets": [
{
"kind": [
"staticlib",
"rlib"
],
"crate_types": [
"staticlib",
"rlib"
],
"name": "rav1d",
"src_path": "/home/user/.cargo/git/checkouts/rav1d-da3745f1281a575f/3a50834/src/lib.rs",
"edition": "2021",
"doc": true,
"doctest": true,
"test": true
},
{
"kind": [
"custom-build"
],
"crate_types": [
"bin"
],
"name": "build-script-build",
"src_path": "/home/user/.cargo/git/checkouts/rav1d-da3745f1281a575f/3a50834/build.rs",
"edition": "2021",
"doc": false,
"doctest": false,
"test": false
}
],
"features": {
"asm": [],
"asm_arm64_dotprod": [
"asm"
],
"asm_arm64_i8mm": [
"asm"
],
"asm_arm64_sve2": [
"asm"
],
"bitdepth_16": [],
"bitdepth_8": [],
"default": [
"asm",
"asm_arm64_dotprod",
"asm_arm64_i8mm",
"asm_arm64_sve2",
"bitdepth_8",
"bitdepth_16"
]
},
"manifest_path": "/home/user/.cargo/git/checkouts/rav1d-da3745f1281a575f/3a50834/Cargo.toml",
"metadata": null,
"publish": null,
"authors": [
"Rav1d Developers",
"Prossimo"
],
"categories": [],
"keywords": [],
"readme": "README.md",
"repository": "https://github.com/memorysafety/rav1d",
"homepage": null,
"documentation": null,
"edition": "2021",
"links": null,
"default_run": null,
"rust_version": "1.79"
}
Loading