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