provenant/parsers/
pip_inspect_deplock.rs1use crate::models::{PackageData, PackageType};
26use std::path::Path;
27
28#[cfg(test)]
29use crate::models::DatasourceId;
30#[cfg(test)]
31use crate::parser_warn as warn;
32#[cfg(test)]
33use serde::{Deserialize, Serialize};
34#[cfg(test)]
35use serde_json::Value;
36
37use super::PackageParser;
38#[cfg(test)]
39use super::license_normalization::normalize_spdx_declared_license;
40use super::metadata::ParserMetadata;
41use super::python::PythonParser;
42#[cfg(test)]
43use super::python::extract_requires_dist_dependencies;
44#[cfg(test)]
45use crate::parsers::utils::{CappedIterExt, truncate_field};
46
47const PACKAGE_TYPE: PackageType = PackageType::Pypi;
48
49#[cfg(test)]
50fn default_package_data() -> PackageData {
51 PackageData {
52 package_type: Some(PACKAGE_TYPE),
53 primary_language: Some("Python".to_string()),
54 datasource_id: Some(DatasourceId::PypiInspectDeplock),
55 ..Default::default()
56 }
57}
58
59pub struct PipInspectDeplockParser;
61
62#[cfg(test)]
63#[derive(Debug, Deserialize, Serialize)]
64struct PipInspectDeplock {
65 installed: Option<Vec<InstalledPackage>>,
66 pip_version: Option<String>,
67 version: Option<String>,
68}
69
70#[cfg(test)]
71#[derive(Debug, Deserialize, Serialize)]
72struct InstalledPackage {
73 metadata: Option<PackageMetadata>,
74 requested: Option<bool>,
75 direct_url: Option<Value>,
76}
77
78#[cfg(test)]
79#[derive(Debug, Deserialize, Serialize)]
80struct PackageMetadata {
81 name: Option<String>,
82 version: Option<String>,
83 license: Option<String>,
84 description: Option<String>,
85 keywords: Option<String>,
86 requires_dist: Option<Vec<String>>,
87}
88
89impl PackageParser for PipInspectDeplockParser {
90 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
91
92 fn metadata() -> Vec<ParserMetadata> {
93 vec![ParserMetadata {
94 description: "pip inspect deplock file",
95 file_patterns: &["*pip-inspect.deplock"],
96 package_type: "pypi",
97 primary_language: "Python",
98 documentation_url: Some("https://pip.pypa.io/en/stable/cli/pip_inspect/"),
99 }]
100 }
101
102 fn is_match(path: &Path) -> bool {
103 path.to_str()
104 .is_some_and(|p| p.ends_with("/pip-inspect.deplock"))
105 }
106
107 fn extract_packages(path: &Path) -> Vec<PackageData> {
108 vec![PythonParser::extract_first_package(path)]
109 }
110}
111
112#[cfg(test)]
113pub(crate) fn parse_pip_inspect_deplock(content: &str) -> PackageData {
114 let data: PipInspectDeplock = match serde_json::from_str(content) {
115 Ok(d) => d,
116 Err(e) => {
117 warn!("Failed to parse pip-inspect.deplock: {}", e);
118 return default_package_data();
119 }
120 };
121
122 let Some(installed_packages) = data.installed else {
123 return default_package_data();
124 };
125
126 let main_package = installed_packages
128 .iter()
129 .capped("pip inspect installed packages")
130 .find(|p| p.requested.unwrap_or(false) && p.direct_url.is_some());
131
132 let metadata = if let Some(pkg) = main_package {
133 pkg.metadata.as_ref()
134 } else {
135 installed_packages
137 .iter()
138 .capped("pip inspect installed packages")
139 .find(|p| p.requested.unwrap_or(false))
140 .and_then(|p| p.metadata.as_ref())
141 };
142
143 let Some(metadata) = metadata else {
144 return default_package_data();
145 };
146
147 let mut extra_data = std::collections::HashMap::new();
149 if let Some(ref pip_version) = data.pip_version {
150 extra_data.insert(
151 "pip_version".to_string(),
152 Value::String(pip_version.clone()),
153 );
154 }
155 if let Some(ref inspect_version) = data.version {
156 extra_data.insert(
157 "inspect_version".to_string(),
158 Value::String(inspect_version.clone()),
159 );
160 }
161
162 let extra_data_opt = if extra_data.is_empty() {
163 None
164 } else {
165 Some(extra_data)
166 };
167
168 let keywords = metadata
169 .keywords
170 .as_ref()
171 .map(|k| vec![truncate_field(k.clone())])
172 .unwrap_or_default();
173 let (declared_license_expression, declared_license_expression_spdx, license_detections) =
174 normalize_spdx_declared_license(metadata.license.as_deref());
175 let dependencies = metadata
176 .requires_dist
177 .as_ref()
178 .map(|requires_dist| extract_requires_dist_dependencies(requires_dist))
179 .unwrap_or_default();
180
181 PackageData {
182 package_type: Some(PACKAGE_TYPE),
183 primary_language: Some("Python".to_string()),
184 name: metadata.name.as_ref().map(|v| truncate_field(v.clone())),
185 version: metadata.version.as_ref().map(|v| truncate_field(v.clone())),
186 declared_license_expression,
187 declared_license_expression_spdx,
188 license_detections,
189 extracted_license_statement: metadata.license.as_ref().map(|v| truncate_field(v.clone())),
190 description: metadata
191 .description
192 .as_ref()
193 .map(|v| truncate_field(v.clone())),
194 keywords,
195 is_virtual: true,
196 extra_data: extra_data_opt,
197 dependencies,
198 datasource_id: Some(DatasourceId::PypiInspectDeplock),
199 ..Default::default()
200 }
201}