Skip to main content

socket_patch_core/utils/
purl.rs

1/// Strip query string qualifiers from a PURL.
2///
3/// e.g., `"pkg:pypi/requests@2.28.0?artifact_id=abc"` -> `"pkg:pypi/requests@2.28.0"`
4pub fn strip_purl_qualifiers(purl: &str) -> &str {
5    match purl.find('?') {
6        Some(idx) => &purl[..idx],
7        None => purl,
8    }
9}
10
11/// Check if a PURL is a PyPI package.
12pub fn is_pypi_purl(purl: &str) -> bool {
13    purl.starts_with("pkg:pypi/")
14}
15
16/// Check if a PURL is an npm package.
17pub fn is_npm_purl(purl: &str) -> bool {
18    purl.starts_with("pkg:npm/")
19}
20
21/// Parse a PyPI PURL to extract name and version.
22///
23/// e.g., `"pkg:pypi/requests@2.28.0?artifact_id=abc"` -> `Some(("requests", "2.28.0"))`
24pub fn parse_pypi_purl(purl: &str) -> Option<(&str, &str)> {
25    let base = strip_purl_qualifiers(purl);
26    let rest = base.strip_prefix("pkg:pypi/")?;
27    let at_idx = rest.rfind('@')?;
28    let name = &rest[..at_idx];
29    let version = &rest[at_idx + 1..];
30    if name.is_empty() || version.is_empty() {
31        return None;
32    }
33    Some((name, version))
34}
35
36/// Parse an npm PURL to extract namespace, name, and version.
37///
38/// e.g., `"pkg:npm/@types/node@20.0.0"` -> `Some((Some("@types"), "node", "20.0.0"))`
39/// e.g., `"pkg:npm/lodash@4.17.21"` -> `Some((None, "lodash", "4.17.21"))`
40pub fn parse_npm_purl(purl: &str) -> Option<(Option<&str>, &str, &str)> {
41    let base = strip_purl_qualifiers(purl);
42    let rest = base.strip_prefix("pkg:npm/")?;
43
44    // Find the last @ that separates name from version
45    let at_idx = rest.rfind('@')?;
46    let name_part = &rest[..at_idx];
47    let version = &rest[at_idx + 1..];
48
49    if name_part.is_empty() || version.is_empty() {
50        return None;
51    }
52
53    // Check for scoped package (@scope/name)
54    if name_part.starts_with('@') {
55        let slash_idx = name_part.find('/')?;
56        let namespace = &name_part[..slash_idx];
57        let name = &name_part[slash_idx + 1..];
58        if name.is_empty() {
59            return None;
60        }
61        Some((Some(namespace), name, version))
62    } else {
63        Some((None, name_part, version))
64    }
65}
66
67/// Check if a PURL is a Ruby gem.
68pub fn is_gem_purl(purl: &str) -> bool {
69    purl.starts_with("pkg:gem/")
70}
71
72/// Parse a gem PURL to extract name and version.
73///
74/// e.g., `"pkg:gem/rails@7.1.0"` -> `Some(("rails", "7.1.0"))`
75pub fn parse_gem_purl(purl: &str) -> Option<(&str, &str)> {
76    let base = strip_purl_qualifiers(purl);
77    let rest = base.strip_prefix("pkg:gem/")?;
78    let at_idx = rest.rfind('@')?;
79    let name = &rest[..at_idx];
80    let version = &rest[at_idx + 1..];
81    if name.is_empty() || version.is_empty() {
82        return None;
83    }
84    Some((name, version))
85}
86
87/// Build a gem PURL from components.
88pub fn build_gem_purl(name: &str, version: &str) -> String {
89    format!("pkg:gem/{name}@{version}")
90}
91
92/// Check if a PURL is a Maven package.
93#[cfg(feature = "maven")]
94pub fn is_maven_purl(purl: &str) -> bool {
95    purl.starts_with("pkg:maven/")
96}
97
98/// Parse a Maven PURL to extract groupId, artifactId, and version.
99///
100/// e.g., `"pkg:maven/org.apache.commons/commons-lang3@3.12.0"` -> `Some(("org.apache.commons", "commons-lang3", "3.12.0"))`
101#[cfg(feature = "maven")]
102pub fn parse_maven_purl(purl: &str) -> Option<(&str, &str, &str)> {
103    let base = strip_purl_qualifiers(purl);
104    let rest = base.strip_prefix("pkg:maven/")?;
105    let at_idx = rest.rfind('@')?;
106    let name_part = &rest[..at_idx];
107    let version = &rest[at_idx + 1..];
108
109    if name_part.is_empty() || version.is_empty() {
110        return None;
111    }
112
113    // Split groupId/artifactId
114    let slash_idx = name_part.find('/')?;
115    let group_id = &name_part[..slash_idx];
116    let artifact_id = &name_part[slash_idx + 1..];
117
118    if group_id.is_empty() || artifact_id.is_empty() {
119        return None;
120    }
121
122    Some((group_id, artifact_id, version))
123}
124
125/// Build a Maven PURL from components.
126#[cfg(feature = "maven")]
127pub fn build_maven_purl(group_id: &str, artifact_id: &str, version: &str) -> String {
128    format!("pkg:maven/{group_id}/{artifact_id}@{version}")
129}
130
131/// Check if a PURL is a Go module.
132#[cfg(feature = "golang")]
133pub fn is_golang_purl(purl: &str) -> bool {
134    purl.starts_with("pkg:golang/")
135}
136
137/// Parse a Go module PURL to extract module path and version.
138///
139/// e.g., `"pkg:golang/github.com/gin-gonic/gin@v1.9.1"` -> `Some(("github.com/gin-gonic/gin", "v1.9.1"))`
140#[cfg(feature = "golang")]
141pub fn parse_golang_purl(purl: &str) -> Option<(&str, &str)> {
142    let base = strip_purl_qualifiers(purl);
143    let rest = base.strip_prefix("pkg:golang/")?;
144    let at_idx = rest.rfind('@')?;
145    let module_path = &rest[..at_idx];
146    let version = &rest[at_idx + 1..];
147    if module_path.is_empty() || version.is_empty() {
148        return None;
149    }
150    Some((module_path, version))
151}
152
153/// Build a Go module PURL from components.
154#[cfg(feature = "golang")]
155pub fn build_golang_purl(module_path: &str, version: &str) -> String {
156    format!("pkg:golang/{module_path}@{version}")
157}
158
159/// Check if a PURL is a Composer/PHP package.
160#[cfg(feature = "composer")]
161pub fn is_composer_purl(purl: &str) -> bool {
162    purl.starts_with("pkg:composer/")
163}
164
165/// Parse a Composer PURL to extract namespace, name, and version.
166///
167/// Composer packages always have a namespace (vendor).
168/// e.g., `"pkg:composer/monolog/monolog@3.5.0"` -> `Some((("monolog", "monolog"), "3.5.0"))`
169#[cfg(feature = "composer")]
170pub fn parse_composer_purl(purl: &str) -> Option<((&str, &str), &str)> {
171    let base = strip_purl_qualifiers(purl);
172    let rest = base.strip_prefix("pkg:composer/")?;
173    let at_idx = rest.rfind('@')?;
174    let name_part = &rest[..at_idx];
175    let version = &rest[at_idx + 1..];
176
177    if name_part.is_empty() || version.is_empty() {
178        return None;
179    }
180
181    // Split namespace/name
182    let slash_idx = name_part.find('/')?;
183    let namespace = &name_part[..slash_idx];
184    let name = &name_part[slash_idx + 1..];
185
186    if namespace.is_empty() || name.is_empty() {
187        return None;
188    }
189
190    Some(((namespace, name), version))
191}
192
193/// Build a Composer PURL from components.
194#[cfg(feature = "composer")]
195pub fn build_composer_purl(namespace: &str, name: &str, version: &str) -> String {
196    format!("pkg:composer/{namespace}/{name}@{version}")
197}
198
199/// Check if a PURL is a NuGet/.NET package.
200#[cfg(feature = "nuget")]
201pub fn is_nuget_purl(purl: &str) -> bool {
202    purl.starts_with("pkg:nuget/")
203}
204
205/// Parse a NuGet PURL to extract name and version.
206///
207/// e.g., `"pkg:nuget/Newtonsoft.Json@13.0.3"` -> `Some(("Newtonsoft.Json", "13.0.3"))`
208#[cfg(feature = "nuget")]
209pub fn parse_nuget_purl(purl: &str) -> Option<(&str, &str)> {
210    let base = strip_purl_qualifiers(purl);
211    let rest = base.strip_prefix("pkg:nuget/")?;
212    let at_idx = rest.rfind('@')?;
213    let name = &rest[..at_idx];
214    let version = &rest[at_idx + 1..];
215    if name.is_empty() || version.is_empty() {
216        return None;
217    }
218    Some((name, version))
219}
220
221/// Build a NuGet PURL from components.
222#[cfg(feature = "nuget")]
223pub fn build_nuget_purl(name: &str, version: &str) -> String {
224    format!("pkg:nuget/{name}@{version}")
225}
226
227/// Check if a PURL is a Cargo/Rust crate.
228#[cfg(feature = "cargo")]
229pub fn is_cargo_purl(purl: &str) -> bool {
230    purl.starts_with("pkg:cargo/")
231}
232
233/// Parse a Cargo PURL to extract name and version.
234///
235/// e.g., `"pkg:cargo/serde@1.0.200"` -> `Some(("serde", "1.0.200"))`
236#[cfg(feature = "cargo")]
237pub fn parse_cargo_purl(purl: &str) -> Option<(&str, &str)> {
238    let base = strip_purl_qualifiers(purl);
239    let rest = base.strip_prefix("pkg:cargo/")?;
240    let at_idx = rest.rfind('@')?;
241    let name = &rest[..at_idx];
242    let version = &rest[at_idx + 1..];
243    if name.is_empty() || version.is_empty() {
244        return None;
245    }
246    Some((name, version))
247}
248
249/// Build a Cargo PURL from components.
250#[cfg(feature = "cargo")]
251pub fn build_cargo_purl(name: &str, version: &str) -> String {
252    format!("pkg:cargo/{name}@{version}")
253}
254
255/// Parse a PURL into ecosystem, package directory path, and version.
256/// Supports npm, pypi, and (with `cargo` feature) cargo PURLs.
257pub fn parse_purl(purl: &str) -> Option<(&str, String, &str)> {
258    let base = strip_purl_qualifiers(purl);
259    if let Some(rest) = base.strip_prefix("pkg:npm/") {
260        let at_idx = rest.rfind('@')?;
261        let pkg_dir = &rest[..at_idx];
262        let version = &rest[at_idx + 1..];
263        if pkg_dir.is_empty() || version.is_empty() {
264            return None;
265        }
266        Some(("npm", pkg_dir.to_string(), version))
267    } else if let Some(rest) = base.strip_prefix("pkg:pypi/") {
268        let at_idx = rest.rfind('@')?;
269        let name = &rest[..at_idx];
270        let version = &rest[at_idx + 1..];
271        if name.is_empty() || version.is_empty() {
272            return None;
273        }
274        Some(("pypi", name.to_string(), version))
275    } else {
276        #[cfg(feature = "cargo")]
277        if let Some(rest) = base.strip_prefix("pkg:cargo/") {
278            let at_idx = rest.rfind('@')?;
279            let name = &rest[..at_idx];
280            let version = &rest[at_idx + 1..];
281            if name.is_empty() || version.is_empty() {
282                return None;
283            }
284            return Some(("cargo", name.to_string(), version));
285        }
286        #[cfg(feature = "golang")]
287        if let Some(rest) = base.strip_prefix("pkg:golang/") {
288            let at_idx = rest.rfind('@')?;
289            let module_path = &rest[..at_idx];
290            let version = &rest[at_idx + 1..];
291            if module_path.is_empty() || version.is_empty() {
292                return None;
293            }
294            return Some(("golang", module_path.to_string(), version));
295        }
296        if let Some(rest) = base.strip_prefix("pkg:gem/") {
297            let at_idx = rest.rfind('@')?;
298            let name = &rest[..at_idx];
299            let version = &rest[at_idx + 1..];
300            if name.is_empty() || version.is_empty() {
301                return None;
302            }
303            return Some(("gem", name.to_string(), version));
304        }
305        #[cfg(feature = "maven")]
306        if let Some(rest) = base.strip_prefix("pkg:maven/") {
307            let at_idx = rest.rfind('@')?;
308            let name_part = &rest[..at_idx];
309            let version = &rest[at_idx + 1..];
310            if name_part.is_empty() || version.is_empty() {
311                return None;
312            }
313            return Some(("maven", name_part.to_string(), version));
314        }
315        #[cfg(feature = "composer")]
316        if let Some(rest) = base.strip_prefix("pkg:composer/") {
317            let at_idx = rest.rfind('@')?;
318            let name_part = &rest[..at_idx];
319            let version = &rest[at_idx + 1..];
320            if name_part.is_empty() || version.is_empty() {
321                return None;
322            }
323            return Some(("composer", name_part.to_string(), version));
324        }
325        #[cfg(feature = "nuget")]
326        if let Some(rest) = base.strip_prefix("pkg:nuget/") {
327            let at_idx = rest.rfind('@')?;
328            let name = &rest[..at_idx];
329            let version = &rest[at_idx + 1..];
330            if name.is_empty() || version.is_empty() {
331                return None;
332            }
333            return Some(("nuget", name.to_string(), version));
334        }
335        None
336    }
337}
338
339/// Check if a string looks like a PURL.
340pub fn is_purl(s: &str) -> bool {
341    s.starts_with("pkg:")
342}
343
344/// Build an npm PURL from components.
345pub fn build_npm_purl(namespace: Option<&str>, name: &str, version: &str) -> String {
346    match namespace {
347        Some(ns) => format!("pkg:npm/{}/{name}@{version}", ns),
348        None => format!("pkg:npm/{name}@{version}"),
349    }
350}
351
352/// Build a PyPI PURL from components.
353pub fn build_pypi_purl(name: &str, version: &str) -> String {
354    format!("pkg:pypi/{name}@{version}")
355}
356
357#[cfg(test)]
358mod tests {
359    use super::*;
360
361    #[test]
362    fn test_strip_qualifiers() {
363        assert_eq!(
364            strip_purl_qualifiers("pkg:pypi/requests@2.28.0?artifact_id=abc"),
365            "pkg:pypi/requests@2.28.0"
366        );
367        assert_eq!(
368            strip_purl_qualifiers("pkg:npm/lodash@4.17.21"),
369            "pkg:npm/lodash@4.17.21"
370        );
371    }
372
373    #[test]
374    fn test_is_pypi_purl() {
375        assert!(is_pypi_purl("pkg:pypi/requests@2.28.0"));
376        assert!(!is_pypi_purl("pkg:npm/lodash@4.17.21"));
377    }
378
379    #[test]
380    fn test_is_npm_purl() {
381        assert!(is_npm_purl("pkg:npm/lodash@4.17.21"));
382        assert!(!is_npm_purl("pkg:pypi/requests@2.28.0"));
383    }
384
385    #[test]
386    fn test_parse_pypi_purl() {
387        assert_eq!(
388            parse_pypi_purl("pkg:pypi/requests@2.28.0"),
389            Some(("requests", "2.28.0"))
390        );
391        assert_eq!(
392            parse_pypi_purl("pkg:pypi/requests@2.28.0?artifact_id=abc"),
393            Some(("requests", "2.28.0"))
394        );
395        assert_eq!(parse_pypi_purl("pkg:npm/lodash@4.17.21"), None);
396        assert_eq!(parse_pypi_purl("pkg:pypi/@2.28.0"), None);
397        assert_eq!(parse_pypi_purl("pkg:pypi/requests@"), None);
398    }
399
400    #[test]
401    fn test_parse_npm_purl() {
402        assert_eq!(
403            parse_npm_purl("pkg:npm/lodash@4.17.21"),
404            Some((None, "lodash", "4.17.21"))
405        );
406        assert_eq!(
407            parse_npm_purl("pkg:npm/@types/node@20.0.0"),
408            Some((Some("@types"), "node", "20.0.0"))
409        );
410        assert_eq!(parse_npm_purl("pkg:pypi/requests@2.28.0"), None);
411    }
412
413    #[test]
414    fn test_parse_purl() {
415        let (eco, dir, ver) = parse_purl("pkg:npm/lodash@4.17.21").unwrap();
416        assert_eq!(eco, "npm");
417        assert_eq!(dir, "lodash");
418        assert_eq!(ver, "4.17.21");
419
420        let (eco, dir, ver) = parse_purl("pkg:npm/@types/node@20.0.0").unwrap();
421        assert_eq!(eco, "npm");
422        assert_eq!(dir, "@types/node");
423        assert_eq!(ver, "20.0.0");
424
425        let (eco, dir, ver) = parse_purl("pkg:pypi/requests@2.28.0").unwrap();
426        assert_eq!(eco, "pypi");
427        assert_eq!(dir, "requests");
428        assert_eq!(ver, "2.28.0");
429    }
430
431    #[test]
432    fn test_is_purl() {
433        assert!(is_purl("pkg:npm/lodash@4.17.21"));
434        assert!(is_purl("pkg:pypi/requests@2.28.0"));
435        assert!(!is_purl("lodash"));
436        assert!(!is_purl("CVE-2024-1234"));
437    }
438
439    #[test]
440    fn test_build_npm_purl() {
441        assert_eq!(
442            build_npm_purl(None, "lodash", "4.17.21"),
443            "pkg:npm/lodash@4.17.21"
444        );
445        assert_eq!(
446            build_npm_purl(Some("@types"), "node", "20.0.0"),
447            "pkg:npm/@types/node@20.0.0"
448        );
449    }
450
451    #[test]
452    fn test_build_pypi_purl() {
453        assert_eq!(
454            build_pypi_purl("requests", "2.28.0"),
455            "pkg:pypi/requests@2.28.0"
456        );
457    }
458
459    #[cfg(feature = "cargo")]
460    #[test]
461    fn test_is_cargo_purl() {
462        assert!(is_cargo_purl("pkg:cargo/serde@1.0.200"));
463        assert!(!is_cargo_purl("pkg:npm/lodash@4.17.21"));
464        assert!(!is_cargo_purl("pkg:pypi/requests@2.28.0"));
465    }
466
467    #[cfg(feature = "cargo")]
468    #[test]
469    fn test_parse_cargo_purl() {
470        assert_eq!(
471            parse_cargo_purl("pkg:cargo/serde@1.0.200"),
472            Some(("serde", "1.0.200"))
473        );
474        assert_eq!(
475            parse_cargo_purl("pkg:cargo/serde_json@1.0.120"),
476            Some(("serde_json", "1.0.120"))
477        );
478        assert_eq!(parse_cargo_purl("pkg:npm/lodash@4.17.21"), None);
479        assert_eq!(parse_cargo_purl("pkg:cargo/@1.0.0"), None);
480        assert_eq!(parse_cargo_purl("pkg:cargo/serde@"), None);
481    }
482
483    #[cfg(feature = "cargo")]
484    #[test]
485    fn test_build_cargo_purl() {
486        assert_eq!(
487            build_cargo_purl("serde", "1.0.200"),
488            "pkg:cargo/serde@1.0.200"
489        );
490    }
491
492    #[cfg(feature = "cargo")]
493    #[test]
494    fn test_cargo_purl_round_trip() {
495        let purl = build_cargo_purl("tokio", "1.38.0");
496        let (name, version) = parse_cargo_purl(&purl).unwrap();
497        assert_eq!(name, "tokio");
498        assert_eq!(version, "1.38.0");
499    }
500
501    #[cfg(feature = "cargo")]
502    #[test]
503    fn test_parse_purl_cargo() {
504        let (eco, dir, ver) = parse_purl("pkg:cargo/serde@1.0.200").unwrap();
505        assert_eq!(eco, "cargo");
506        assert_eq!(dir, "serde");
507        assert_eq!(ver, "1.0.200");
508    }
509
510    #[test]
511    fn test_is_gem_purl() {
512        assert!(is_gem_purl("pkg:gem/rails@7.1.0"));
513        assert!(!is_gem_purl("pkg:npm/lodash@4.17.21"));
514        assert!(!is_gem_purl("pkg:pypi/requests@2.28.0"));
515    }
516
517    #[test]
518    fn test_parse_gem_purl() {
519        assert_eq!(
520            parse_gem_purl("pkg:gem/rails@7.1.0"),
521            Some(("rails", "7.1.0"))
522        );
523        assert_eq!(
524            parse_gem_purl("pkg:gem/nokogiri@1.16.5"),
525            Some(("nokogiri", "1.16.5"))
526        );
527        assert_eq!(parse_gem_purl("pkg:npm/lodash@4.17.21"), None);
528        assert_eq!(parse_gem_purl("pkg:gem/@1.0.0"), None);
529        assert_eq!(parse_gem_purl("pkg:gem/rails@"), None);
530    }
531
532    #[test]
533    fn test_build_gem_purl() {
534        assert_eq!(
535            build_gem_purl("rails", "7.1.0"),
536            "pkg:gem/rails@7.1.0"
537        );
538    }
539
540    #[test]
541    fn test_gem_purl_round_trip() {
542        let purl = build_gem_purl("nokogiri", "1.16.5");
543        let (name, version) = parse_gem_purl(&purl).unwrap();
544        assert_eq!(name, "nokogiri");
545        assert_eq!(version, "1.16.5");
546    }
547
548    #[test]
549    fn test_parse_purl_gem() {
550        let (eco, dir, ver) = parse_purl("pkg:gem/rails@7.1.0").unwrap();
551        assert_eq!(eco, "gem");
552        assert_eq!(dir, "rails");
553        assert_eq!(ver, "7.1.0");
554    }
555
556    #[cfg(feature = "maven")]
557    #[test]
558    fn test_is_maven_purl() {
559        assert!(is_maven_purl("pkg:maven/org.apache.commons/commons-lang3@3.12.0"));
560        assert!(!is_maven_purl("pkg:npm/lodash@4.17.21"));
561        assert!(!is_maven_purl("pkg:pypi/requests@2.28.0"));
562    }
563
564    #[cfg(feature = "maven")]
565    #[test]
566    fn test_parse_maven_purl() {
567        assert_eq!(
568            parse_maven_purl("pkg:maven/org.apache.commons/commons-lang3@3.12.0"),
569            Some(("org.apache.commons", "commons-lang3", "3.12.0"))
570        );
571        assert_eq!(
572            parse_maven_purl("pkg:maven/com.google.guava/guava@32.1.3-jre"),
573            Some(("com.google.guava", "guava", "32.1.3-jre"))
574        );
575        assert_eq!(parse_maven_purl("pkg:npm/lodash@4.17.21"), None);
576        assert_eq!(parse_maven_purl("pkg:maven/@3.12.0"), None);
577        assert_eq!(parse_maven_purl("pkg:maven/org.apache.commons/@3.12.0"), None);
578        assert_eq!(parse_maven_purl("pkg:maven/org.apache.commons/commons-lang3@"), None);
579    }
580
581    #[cfg(feature = "maven")]
582    #[test]
583    fn test_build_maven_purl() {
584        assert_eq!(
585            build_maven_purl("org.apache.commons", "commons-lang3", "3.12.0"),
586            "pkg:maven/org.apache.commons/commons-lang3@3.12.0"
587        );
588    }
589
590    #[cfg(feature = "maven")]
591    #[test]
592    fn test_maven_purl_round_trip() {
593        let purl = build_maven_purl("com.google.guava", "guava", "32.1.3-jre");
594        let (group_id, artifact_id, version) = parse_maven_purl(&purl).unwrap();
595        assert_eq!(group_id, "com.google.guava");
596        assert_eq!(artifact_id, "guava");
597        assert_eq!(version, "32.1.3-jre");
598    }
599
600    #[cfg(feature = "maven")]
601    #[test]
602    fn test_parse_purl_maven() {
603        let (eco, dir, ver) = parse_purl("pkg:maven/org.apache.commons/commons-lang3@3.12.0").unwrap();
604        assert_eq!(eco, "maven");
605        assert_eq!(dir, "org.apache.commons/commons-lang3");
606        assert_eq!(ver, "3.12.0");
607    }
608
609    #[cfg(feature = "golang")]
610    #[test]
611    fn test_is_golang_purl() {
612        assert!(is_golang_purl("pkg:golang/github.com/gin-gonic/gin@v1.9.1"));
613        assert!(!is_golang_purl("pkg:npm/lodash@4.17.21"));
614        assert!(!is_golang_purl("pkg:pypi/requests@2.28.0"));
615    }
616
617    #[cfg(feature = "golang")]
618    #[test]
619    fn test_parse_golang_purl() {
620        assert_eq!(
621            parse_golang_purl("pkg:golang/github.com/gin-gonic/gin@v1.9.1"),
622            Some(("github.com/gin-gonic/gin", "v1.9.1"))
623        );
624        assert_eq!(
625            parse_golang_purl("pkg:golang/golang.org/x/text@v0.14.0"),
626            Some(("golang.org/x/text", "v0.14.0"))
627        );
628        assert_eq!(parse_golang_purl("pkg:npm/lodash@4.17.21"), None);
629        assert_eq!(parse_golang_purl("pkg:golang/@v1.0.0"), None);
630        assert_eq!(parse_golang_purl("pkg:golang/github.com/foo/bar@"), None);
631    }
632
633    #[cfg(feature = "golang")]
634    #[test]
635    fn test_build_golang_purl() {
636        assert_eq!(
637            build_golang_purl("github.com/gin-gonic/gin", "v1.9.1"),
638            "pkg:golang/github.com/gin-gonic/gin@v1.9.1"
639        );
640    }
641
642    #[cfg(feature = "golang")]
643    #[test]
644    fn test_golang_purl_round_trip() {
645        let purl = build_golang_purl("golang.org/x/text", "v0.14.0");
646        let (module_path, version) = parse_golang_purl(&purl).unwrap();
647        assert_eq!(module_path, "golang.org/x/text");
648        assert_eq!(version, "v0.14.0");
649    }
650
651    #[cfg(feature = "golang")]
652    #[test]
653    fn test_parse_purl_golang() {
654        let (eco, dir, ver) = parse_purl("pkg:golang/github.com/gin-gonic/gin@v1.9.1").unwrap();
655        assert_eq!(eco, "golang");
656        assert_eq!(dir, "github.com/gin-gonic/gin");
657        assert_eq!(ver, "v1.9.1");
658    }
659
660    #[cfg(feature = "composer")]
661    #[test]
662    fn test_is_composer_purl() {
663        assert!(is_composer_purl("pkg:composer/monolog/monolog@3.5.0"));
664        assert!(!is_composer_purl("pkg:npm/lodash@4.17.21"));
665        assert!(!is_composer_purl("pkg:pypi/requests@2.28.0"));
666    }
667
668    #[cfg(feature = "composer")]
669    #[test]
670    fn test_parse_composer_purl() {
671        assert_eq!(
672            parse_composer_purl("pkg:composer/monolog/monolog@3.5.0"),
673            Some((("monolog", "monolog"), "3.5.0"))
674        );
675        assert_eq!(
676            parse_composer_purl("pkg:composer/symfony/console@6.4.1"),
677            Some((("symfony", "console"), "6.4.1"))
678        );
679        assert_eq!(parse_composer_purl("pkg:npm/lodash@4.17.21"), None);
680        assert_eq!(parse_composer_purl("pkg:composer/@3.5.0"), None);
681        assert_eq!(parse_composer_purl("pkg:composer/monolog/@3.5.0"), None);
682        assert_eq!(parse_composer_purl("pkg:composer/monolog/monolog@"), None);
683    }
684
685    #[cfg(feature = "composer")]
686    #[test]
687    fn test_build_composer_purl() {
688        assert_eq!(
689            build_composer_purl("monolog", "monolog", "3.5.0"),
690            "pkg:composer/monolog/monolog@3.5.0"
691        );
692    }
693
694    #[cfg(feature = "composer")]
695    #[test]
696    fn test_composer_purl_round_trip() {
697        let purl = build_composer_purl("symfony", "console", "6.4.1");
698        let ((namespace, name), version) = parse_composer_purl(&purl).unwrap();
699        assert_eq!(namespace, "symfony");
700        assert_eq!(name, "console");
701        assert_eq!(version, "6.4.1");
702    }
703
704    #[cfg(feature = "composer")]
705    #[test]
706    fn test_parse_purl_composer() {
707        let (eco, dir, ver) = parse_purl("pkg:composer/monolog/monolog@3.5.0").unwrap();
708        assert_eq!(eco, "composer");
709        assert_eq!(dir, "monolog/monolog");
710        assert_eq!(ver, "3.5.0");
711    }
712
713    #[cfg(feature = "nuget")]
714    #[test]
715    fn test_is_nuget_purl() {
716        assert!(is_nuget_purl("pkg:nuget/Newtonsoft.Json@13.0.3"));
717        assert!(!is_nuget_purl("pkg:npm/lodash@4.17.21"));
718        assert!(!is_nuget_purl("pkg:pypi/requests@2.28.0"));
719    }
720
721    #[cfg(feature = "nuget")]
722    #[test]
723    fn test_parse_nuget_purl() {
724        assert_eq!(
725            parse_nuget_purl("pkg:nuget/Newtonsoft.Json@13.0.3"),
726            Some(("Newtonsoft.Json", "13.0.3"))
727        );
728        assert_eq!(
729            parse_nuget_purl("pkg:nuget/System.Text.Json@8.0.0"),
730            Some(("System.Text.Json", "8.0.0"))
731        );
732        assert_eq!(parse_nuget_purl("pkg:npm/lodash@4.17.21"), None);
733        assert_eq!(parse_nuget_purl("pkg:nuget/@1.0.0"), None);
734        assert_eq!(parse_nuget_purl("pkg:nuget/Newtonsoft.Json@"), None);
735    }
736
737    #[cfg(feature = "nuget")]
738    #[test]
739    fn test_build_nuget_purl() {
740        assert_eq!(
741            build_nuget_purl("Newtonsoft.Json", "13.0.3"),
742            "pkg:nuget/Newtonsoft.Json@13.0.3"
743        );
744    }
745
746    #[cfg(feature = "nuget")]
747    #[test]
748    fn test_nuget_purl_round_trip() {
749        let purl = build_nuget_purl("System.Text.Json", "8.0.0");
750        let (name, version) = parse_nuget_purl(&purl).unwrap();
751        assert_eq!(name, "System.Text.Json");
752        assert_eq!(version, "8.0.0");
753    }
754
755    #[cfg(feature = "nuget")]
756    #[test]
757    fn test_parse_purl_nuget() {
758        let (eco, dir, ver) = parse_purl("pkg:nuget/Newtonsoft.Json@13.0.3").unwrap();
759        assert_eq!(eco, "nuget");
760        assert_eq!(dir, "Newtonsoft.Json");
761        assert_eq!(ver, "13.0.3");
762    }
763}