Skip to content

Commit 4860bec

Browse files
authored
Merge pull request #856 from Shnatsel/correct-vcs-url-in-purl
Do not encode `?qualifier=` strings into the vcs_url field of a PURL
2 parents d4d764c + 6909748 commit 4860bec

2 files changed

Lines changed: 340 additions & 1 deletion

File tree

cargo-cyclonedx/src/purl.rs

Lines changed: 70 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,51 @@ pub fn get_purl(
6565
}
6666

6767
/// Converts the `cargo metadata`'s `source` field to a valid PURL `vcs_url`.
68+
///
69+
/// The `vcs_url` qualifier is specified to use the SPDX Package Download Location format:
70+
/// `<vcs_tool>+<transport>://<host_name>[/<path_to_repository>][@<revision_tag_or_branch>][#<sub_path>]`
71+
///
72+
/// Cargo metadata uses a different format:
73+
/// `git+<url>[?branch=<branch>|?tag=<tag>|?rev=<rev>]#<commit_hash>`
74+
///
75+
/// This function strips the query parameters (since the commit hash already identifies the code)
76+
/// and converts the `#commit_hash` to `@commit_hash` per the SPDX format.
77+
///
6878
/// Assumes that the source kind is `git`, panics if it isn't.
6979
fn source_to_vcs_url(source: &cargo_metadata::Source) -> String {
7080
assert!(source.repr.starts_with("git+"));
71-
source.repr.replace('#', "@")
81+
let url = &source.repr;
82+
// Find where query parameters start (if any) and where the commit hash fragment starts
83+
let query_start = url.find('?');
84+
let fragment_start = url.find('#');
85+
match (query_start, fragment_start) {
86+
// Has both query params and commit hash: strip query, keep commit as @
87+
(Some(q), Some(f)) => {
88+
let base = &url[..q];
89+
let commit = &url[f + 1..];
90+
format!("{}@{}", base, commit)
91+
}
92+
// No query params, has commit hash: just replace # with @
93+
(None, Some(_)) => url.replace('#', "@"),
94+
// Has query params but no commit hash: extract the ref value as @
95+
(Some(q), None) => {
96+
let base = &url[..q];
97+
let query = &url[q + 1..];
98+
// Extract the value from branch=X, tag=X, or rev=X
99+
let ref_value = query
100+
.split('&')
101+
.find_map(|param| {
102+
param
103+
.strip_prefix("branch=")
104+
.or_else(|| param.strip_prefix("tag="))
105+
.or_else(|| param.strip_prefix("rev="))
106+
})
107+
.unwrap_or(query);
108+
format!("{}@{}", base, ref_value)
109+
}
110+
// No query params, no commit hash: return as-is
111+
(None, None) => url.to_string(),
112+
}
72113
}
73114

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

92133
const CRATES_IO_PACKAGE_JSON: &str = include_str!("../tests/fixtures/crates_io_package.json");
93134
const GIT_PACKAGE_JSON: &str = include_str!("../tests/fixtures/git_package.json");
135+
const GIT_PACKAGE_WITH_BRANCH_JSON: &str =
136+
include_str!("../tests/fixtures/git_package_with_branch.json");
94137
const ROOT_PACKAGE_JSON: &str = include_str!("../tests/fixtures/root_package.json");
95138
const WORKSPACE_PACKAGE_JSON: &str = include_str!("../tests/fixtures/workspace_package.json");
96139

@@ -129,6 +172,32 @@ mod tests {
129172
assert!(parsed_purl.namespace().is_none());
130173
}
131174

175+
#[test]
176+
fn git_purl_with_branch() {
177+
let git_package: Package = serde_json::from_str(GIT_PACKAGE_WITH_BRANCH_JSON).unwrap();
178+
let purl = get_purl(&git_package, &git_package, Utf8Path::new("/foo/bar"), None).unwrap();
179+
// Validate that data roundtripped correctly
180+
let parsed_purl = Purl::from_str(purl.as_ref()).unwrap();
181+
assert_eq!(parsed_purl.name(), "rav1d");
182+
assert_eq!(parsed_purl.version(), Some("1.1.0"));
183+
assert_eq!(parsed_purl.qualifiers().len(), 1);
184+
let (qualifier, value) = parsed_purl.qualifiers().iter().next().unwrap();
185+
assert_eq!(qualifier.as_str(), "vcs_url");
186+
// The ?branch= query param must be stripped; only the commit hash remains after @
187+
let decoded_value = percent_decode(value.as_bytes())
188+
.decode_utf8()
189+
.unwrap()
190+
.to_string();
191+
assert_eq!(
192+
decoded_value,
193+
"git+https://github.com/leo030303/rav1d.git@3a50834ce3743bc580f340ba3bfbdbf6a46ab783"
194+
);
195+
// Ensure ?branch= is NOT present in the vcs_url
196+
assert!(!decoded_value.contains("?branch="));
197+
assert!(parsed_purl.subpath().is_none());
198+
assert!(parsed_purl.namespace().is_none());
199+
}
200+
132201
#[test]
133202
fn toplevel_package_purl() {
134203
let root_package: Package = serde_json::from_str(ROOT_PACKAGE_JSON).unwrap();
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
{
2+
"name": "rav1d",
3+
"version": "1.1.0",
4+
"id": "git+https://github.com/leo030303/rav1d.git?branch=add-rust-api#rav1d@1.1.0",
5+
"license": "BSD-2-Clause",
6+
"license_file": null,
7+
"description": "Rust port of the dav1d AV1 decoder",
8+
"source": "git+https://github.com/leo030303/rav1d.git?branch=add-rust-api#3a50834ce3743bc580f340ba3bfbdbf6a46ab783",
9+
"dependencies": [
10+
{
11+
"name": "assert_matches",
12+
"source": "registry+https://github.com/rust-lang/crates.io-index",
13+
"req": "^1.5.0",
14+
"kind": null,
15+
"rename": null,
16+
"optional": false,
17+
"uses_default_features": true,
18+
"features": [],
19+
"target": null,
20+
"registry": null
21+
},
22+
{
23+
"name": "atomig",
24+
"source": "registry+https://github.com/rust-lang/crates.io-index",
25+
"req": "^0.4.0",
26+
"kind": null,
27+
"rename": null,
28+
"optional": false,
29+
"uses_default_features": true,
30+
"features": [
31+
"derive"
32+
],
33+
"target": null,
34+
"registry": null
35+
},
36+
{
37+
"name": "av-data",
38+
"source": "registry+https://github.com/rust-lang/crates.io-index",
39+
"req": "^0.4.2",
40+
"kind": null,
41+
"rename": null,
42+
"optional": false,
43+
"uses_default_features": true,
44+
"features": [],
45+
"target": null,
46+
"registry": null
47+
},
48+
{
49+
"name": "bitflags",
50+
"source": "registry+https://github.com/rust-lang/crates.io-index",
51+
"req": "^2.4.0",
52+
"kind": null,
53+
"rename": null,
54+
"optional": false,
55+
"uses_default_features": true,
56+
"features": [],
57+
"target": null,
58+
"registry": null
59+
},
60+
{
61+
"name": "cfg-if",
62+
"source": "registry+https://github.com/rust-lang/crates.io-index",
63+
"req": "^1.0.0",
64+
"kind": null,
65+
"rename": null,
66+
"optional": false,
67+
"uses_default_features": true,
68+
"features": [],
69+
"target": null,
70+
"registry": null
71+
},
72+
{
73+
"name": "libc",
74+
"source": "registry+https://github.com/rust-lang/crates.io-index",
75+
"req": "^0.2",
76+
"kind": null,
77+
"rename": null,
78+
"optional": false,
79+
"uses_default_features": true,
80+
"features": [],
81+
"target": null,
82+
"registry": null
83+
},
84+
{
85+
"name": "parking_lot",
86+
"source": "registry+https://github.com/rust-lang/crates.io-index",
87+
"req": "^0.12.2",
88+
"kind": null,
89+
"rename": null,
90+
"optional": false,
91+
"uses_default_features": true,
92+
"features": [],
93+
"target": null,
94+
"registry": null
95+
},
96+
{
97+
"name": "paste",
98+
"source": "registry+https://github.com/rust-lang/crates.io-index",
99+
"req": "^1.0.14",
100+
"kind": null,
101+
"rename": null,
102+
"optional": false,
103+
"uses_default_features": true,
104+
"features": [],
105+
"target": null,
106+
"registry": null
107+
},
108+
{
109+
"name": "raw-cpuid",
110+
"source": "registry+https://github.com/rust-lang/crates.io-index",
111+
"req": "^11.0.1",
112+
"kind": null,
113+
"rename": null,
114+
"optional": false,
115+
"uses_default_features": true,
116+
"features": [],
117+
"target": null,
118+
"registry": null
119+
},
120+
{
121+
"name": "static_assertions",
122+
"source": "registry+https://github.com/rust-lang/crates.io-index",
123+
"req": "^1.1.0",
124+
"kind": null,
125+
"rename": null,
126+
"optional": false,
127+
"uses_default_features": true,
128+
"features": [],
129+
"target": null,
130+
"registry": null
131+
},
132+
{
133+
"name": "strum",
134+
"source": "registry+https://github.com/rust-lang/crates.io-index",
135+
"req": "^0.27",
136+
"kind": null,
137+
"rename": null,
138+
"optional": false,
139+
"uses_default_features": true,
140+
"features": [
141+
"derive"
142+
],
143+
"target": null,
144+
"registry": null
145+
},
146+
{
147+
"name": "to_method",
148+
"source": "registry+https://github.com/rust-lang/crates.io-index",
149+
"req": "^1.1.0",
150+
"kind": null,
151+
"rename": null,
152+
"optional": false,
153+
"uses_default_features": true,
154+
"features": [],
155+
"target": null,
156+
"registry": null
157+
},
158+
{
159+
"name": "zerocopy",
160+
"source": "registry+https://github.com/rust-lang/crates.io-index",
161+
"req": "^0.7.32",
162+
"kind": null,
163+
"rename": null,
164+
"optional": false,
165+
"uses_default_features": true,
166+
"features": [
167+
"derive"
168+
],
169+
"target": null,
170+
"registry": null
171+
},
172+
{
173+
"name": "cc",
174+
"source": "registry+https://github.com/rust-lang/crates.io-index",
175+
"req": "^1.0.79",
176+
"kind": "build",
177+
"rename": null,
178+
"optional": false,
179+
"uses_default_features": true,
180+
"features": [],
181+
"target": null,
182+
"registry": null
183+
},
184+
{
185+
"name": "nasm-rs",
186+
"source": "registry+https://github.com/rust-lang/crates.io-index",
187+
"req": "^0.3",
188+
"kind": "build",
189+
"rename": null,
190+
"optional": false,
191+
"uses_default_features": true,
192+
"features": [
193+
"parallel"
194+
],
195+
"target": null,
196+
"registry": null
197+
}
198+
],
199+
"targets": [
200+
{
201+
"kind": [
202+
"staticlib",
203+
"rlib"
204+
],
205+
"crate_types": [
206+
"staticlib",
207+
"rlib"
208+
],
209+
"name": "rav1d",
210+
"src_path": "/home/user/.cargo/git/checkouts/rav1d-da3745f1281a575f/3a50834/src/lib.rs",
211+
"edition": "2021",
212+
"doc": true,
213+
"doctest": true,
214+
"test": true
215+
},
216+
{
217+
"kind": [
218+
"custom-build"
219+
],
220+
"crate_types": [
221+
"bin"
222+
],
223+
"name": "build-script-build",
224+
"src_path": "/home/user/.cargo/git/checkouts/rav1d-da3745f1281a575f/3a50834/build.rs",
225+
"edition": "2021",
226+
"doc": false,
227+
"doctest": false,
228+
"test": false
229+
}
230+
],
231+
"features": {
232+
"asm": [],
233+
"asm_arm64_dotprod": [
234+
"asm"
235+
],
236+
"asm_arm64_i8mm": [
237+
"asm"
238+
],
239+
"asm_arm64_sve2": [
240+
"asm"
241+
],
242+
"bitdepth_16": [],
243+
"bitdepth_8": [],
244+
"default": [
245+
"asm",
246+
"asm_arm64_dotprod",
247+
"asm_arm64_i8mm",
248+
"asm_arm64_sve2",
249+
"bitdepth_8",
250+
"bitdepth_16"
251+
]
252+
},
253+
"manifest_path": "/home/user/.cargo/git/checkouts/rav1d-da3745f1281a575f/3a50834/Cargo.toml",
254+
"metadata": null,
255+
"publish": null,
256+
"authors": [
257+
"Rav1d Developers",
258+
"Prossimo"
259+
],
260+
"categories": [],
261+
"keywords": [],
262+
"readme": "README.md",
263+
"repository": "https://github.com/memorysafety/rav1d",
264+
"homepage": null,
265+
"documentation": null,
266+
"edition": "2021",
267+
"links": null,
268+
"default_run": null,
269+
"rust_version": "1.79"
270+
}

0 commit comments

Comments
 (0)