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