provenant/parsers/nuget/
packages_lock.rs1use std::path::Path;
2
3use crate::models::{DatasourceId, Dependency, PackageData, PackageType};
4use crate::parser_warn as warn;
5use packageurl::PackageUrl;
6
7use super::super::PackageParser;
8use super::super::utils::{MAX_ITERATION_COUNT, read_file_to_string};
9use super::default_package_data;
10
11pub struct PackagesLockParser;
12
13impl PackageParser for PackagesLockParser {
14 const PACKAGE_TYPE: PackageType = PackageType::Nuget;
15
16 fn is_match(path: &Path) -> bool {
17 path.file_name()
18 .and_then(|name| name.to_str())
19 .is_some_and(|name| name.ends_with("packages.lock.json"))
20 }
21
22 fn extract_packages(path: &Path) -> Vec<PackageData> {
23 let content = match read_file_to_string(path, None) {
24 Ok(c) => c,
25 Err(e) => {
26 warn!("Failed to read packages.lock.json at {:?}: {}", path, e);
27 return vec![default_package_data(Some(DatasourceId::NugetPackagesLock))];
28 }
29 };
30
31 let parsed: serde_json::Value = match serde_json::from_str(&content) {
32 Ok(v) => v,
33 Err(e) => {
34 warn!("Failed to parse packages.lock.json at {:?}: {}", path, e);
35 return vec![default_package_data(Some(DatasourceId::NugetPackagesLock))];
36 }
37 };
38
39 let mut dependencies = Vec::new();
40 let mut iteration_count: usize = 0;
41
42 if let Some(deps_obj) = parsed.get("dependencies").and_then(|v| v.as_object()) {
43 for (target_framework, packages) in deps_obj.iter().take(MAX_ITERATION_COUNT) {
44 if let Some(packages_obj) = packages.as_object() {
45 for (package_name, package_info) in
46 packages_obj.iter().take(MAX_ITERATION_COUNT)
47 {
48 iteration_count += 1;
49 if iteration_count > MAX_ITERATION_COUNT {
50 warn!(
51 "Iteration limit exceeded in packages.lock.json at {:?}; stopping at {} dependencies",
52 path, MAX_ITERATION_COUNT
53 );
54 break;
55 }
56 if let Some(info_obj) = package_info.as_object() {
57 let version = info_obj
58 .get("resolved")
59 .and_then(|v| v.as_str())
60 .map(|s| s.to_string());
61
62 let requested = info_obj
63 .get("requested")
64 .and_then(|v| v.as_str())
65 .map(|s| s.to_string());
66
67 let package_type = info_obj.get("type").and_then(|v| v.as_str());
68
69 let is_direct = match package_type {
70 Some("Direct") => Some(true),
71 Some("Transitive") => Some(false),
72 _ => None,
73 };
74
75 let purl = version.as_ref().and_then(|v| {
76 PackageUrl::new("nuget", package_name).ok().map(|mut p| {
77 let _ = p.with_version(v);
78 p.to_string()
79 })
80 });
81
82 let mut extra_data = serde_json::Map::new();
83 extra_data.insert(
84 "target_framework".to_string(),
85 serde_json::Value::String(target_framework.clone()),
86 );
87
88 if let Some(content_hash) =
89 info_obj.get("contentHash").and_then(|v| v.as_str())
90 {
91 extra_data.insert(
92 "content_hash".to_string(),
93 serde_json::Value::String(content_hash.to_string()),
94 );
95 }
96
97 dependencies.push(Dependency {
98 purl,
99 extracted_requirement: requested.or(version),
100 scope: Some(target_framework.clone()),
101 is_runtime: Some(true),
102 is_optional: Some(false),
103 is_pinned: Some(true),
104 is_direct,
105 resolved_package: None,
106 extra_data: if extra_data.is_empty() {
107 None
108 } else {
109 Some(extra_data.into_iter().collect())
110 },
111 });
112 }
113 }
114 }
115 }
116 }
117
118 vec![PackageData {
119 datasource_id: Some(DatasourceId::NugetPackagesLock),
120 package_type: Some(Self::PACKAGE_TYPE),
121 dependencies,
122 ..default_package_data(Some(DatasourceId::NugetPackagesLock))
123 }]
124 }
125}
126
127crate::register_parser!(
128 ".NET packages.lock.json lockfile",
129 &["**/packages.lock.json"],
130 "nuget",
131 "C#",
132 Some(
133 "https://learn.microsoft.com/en-us/nuget/consume-packages/package-references-in-project-files#locking-dependencies"
134 ),
135);