1use std::path::Path;
21
22use crate::parser_warn as warn;
23use serde::{Deserialize, Serialize};
24
25use crate::models::{DatasourceId, Dependency, PackageData, PackageType, ResolvedPackage};
26use crate::parsers::utils::{
27 MAX_ITERATION_COUNT, RecursionGuard, capped_iteration_limit, read_file_to_string,
28 truncate_field,
29};
30
31use super::PackageParser;
32
33const PACKAGE_TYPE: PackageType = PackageType::Swift;
34
35fn default_package_data() -> PackageData {
36 PackageData {
37 package_type: Some(PACKAGE_TYPE),
38 primary_language: Some("Swift".to_string()),
39 datasource_id: Some(DatasourceId::SwiftPackageShowDependencies),
40 ..Default::default()
41 }
42}
43
44pub struct SwiftShowDependenciesParser;
45
46#[derive(Debug, Deserialize, Serialize)]
47struct SwiftDeplock {
48 name: Option<String>,
49 version: Option<String>,
50 url: Option<String>,
51 #[serde(default)]
52 dependencies: Vec<SwiftDependency>,
53}
54
55#[derive(Debug, Deserialize, Serialize, Clone)]
56struct SwiftDependency {
57 identity: Option<String>,
58 name: Option<String>,
59 version: Option<String>,
60 url: Option<String>,
61 #[serde(default)]
62 dependencies: Vec<SwiftDependency>,
63}
64
65impl PackageParser for SwiftShowDependenciesParser {
66 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
67
68 fn is_match(path: &Path) -> bool {
69 path.file_name()
70 .and_then(|name| name.to_str())
71 .is_some_and(|name| name == "swift-show-dependencies.deplock")
72 }
73
74 fn extract_packages(path: &Path) -> Vec<PackageData> {
75 let content = match read_file_to_string(path, None) {
76 Ok(c) => c,
77 Err(e) => {
78 warn!(
79 "Failed to read swift-show-dependencies.deplock {:?}: {}",
80 path, e
81 );
82 return vec![default_package_data()];
83 }
84 };
85
86 vec![parse_swift_show_dependencies(&content)]
87 }
88
89 fn metadata() -> Vec<super::metadata::ParserMetadata> {
90 vec![super::metadata::ParserMetadata {
91 description: "Swift show-dependencies deplock file",
92 file_patterns: &["*swift-show-dependencies.deplock"],
93 package_type: "swift",
94 primary_language: "Swift",
95 documentation_url: Some(
96 "https://forums.swift.org/t/swiftpm-show-dependencies-without-fetching-dependencies/51154",
97 ),
98 }]
99 }
100}
101
102pub(crate) fn parse_swift_show_dependencies(content: &str) -> PackageData {
103 let data: SwiftDeplock = match serde_json::from_str(content) {
104 Ok(d) => d,
105 Err(e) => {
106 warn!("Failed to parse swift-show-dependencies.deplock: {}", e);
107 return default_package_data();
108 }
109 };
110
111 let dependencies = flatten_dependencies(&data.dependencies);
112 let version = normalize_version(data.version);
113 let homepage_url = normalize_remote_url(data.url.clone());
114 let purl = create_root_purl(
115 data.name.as_deref(),
116 data.url.as_deref(),
117 version.as_deref(),
118 );
119
120 PackageData {
121 package_type: Some(PACKAGE_TYPE),
122 primary_language: Some("Swift".to_string()),
123 name: data.name.map(truncate_field),
124 version: version.map(truncate_field),
125 homepage_url: homepage_url.map(truncate_field),
126 dependencies,
127 datasource_id: Some(DatasourceId::SwiftPackageShowDependencies),
128 purl,
129 ..Default::default()
130 }
131}
132
133fn flatten_dependencies(deps: &[SwiftDependency]) -> Vec<Dependency> {
134 let mut result = Vec::new();
135
136 for dep in deps {
137 let mut guard: RecursionGuard<String> = RecursionGuard::new();
138 let mut depth_guard = RecursionGuard::<()>::depth_only();
139 flatten_dependency(dep, true, &mut result, &mut guard, &mut depth_guard);
140 }
141
142 result
143}
144
145fn flatten_dependency(
146 dep: &SwiftDependency,
147 is_direct: bool,
148 result: &mut Vec<Dependency>,
149 guard: &mut RecursionGuard<String>,
150 depth_guard: &mut RecursionGuard<()>,
151) {
152 if guard.exceeded() || depth_guard.exceeded() {
153 warn!("Recursion depth exceeded in swift dependency flattening");
154 return;
155 }
156
157 if result.len() >= MAX_ITERATION_COUNT {
158 return;
159 }
160
161 let dep_key = dep
162 .identity
163 .as_deref()
164 .or(dep.name.as_deref())
165 .unwrap_or("")
166 .to_string();
167 if !dep_key.is_empty() && guard.enter(dep_key.clone()) {
168 return;
169 }
170
171 if let Some(dependency) = build_dependency(dep, is_direct, depth_guard) {
172 result.push(dependency);
173 }
174
175 for child in &dep.dependencies {
176 flatten_dependency(child, false, result, guard, depth_guard);
177 }
178
179 if !dep_key.is_empty() {
180 guard.leave(dep_key);
181 }
182}
183
184fn build_dependency(
185 dep: &SwiftDependency,
186 is_direct: bool,
187 guard: &mut RecursionGuard<()>,
188) -> Option<Dependency> {
189 if guard.descend() {
190 warn!("Recursion depth exceeded in swift dependency building");
191 return None;
192 }
193
194 let name = truncate_field(dep.name.as_ref()?.clone());
195 let version = normalize_version(dep.version.clone());
196 let purl = create_dependency_purl(dep, version.as_deref());
197 let nested_limit = capped_iteration_limit(
198 dep.dependencies.len(),
199 "swift show-dependencies nested dependencies",
200 );
201 let nested_dependencies = dep
202 .dependencies
203 .iter()
204 .take(nested_limit)
205 .filter_map(|child| build_dependency(child, true, guard))
206 .collect();
207
208 guard.ascend();
209
210 Some(Dependency {
211 purl: purl.map(truncate_field),
212 extracted_requirement: version.clone().map(truncate_field),
213 scope: Some("dependencies".to_string()),
214 is_runtime: None,
215 is_optional: None,
216 is_pinned: Some(version.is_some()),
217 is_direct: Some(is_direct),
218 resolved_package: Some(Box::new(ResolvedPackage {
219 primary_language: Some("Swift".to_string()),
220 download_url: None,
221 sha1: None,
222 sha256: None,
223 sha512: None,
224 md5: None,
225 is_virtual: true,
226 extra_data: None,
227 dependencies: nested_dependencies,
228 repository_homepage_url: None,
229 repository_download_url: None,
230 api_data_url: None,
231 datasource_id: Some(DatasourceId::SwiftPackageShowDependencies),
232 purl: None,
233 ..ResolvedPackage::new(
234 PACKAGE_TYPE,
235 truncate_field(extract_namespace(dep.url.as_deref()).unwrap_or_default()),
236 name,
237 truncate_field(version.clone().unwrap_or_default()),
238 )
239 })),
240 extra_data: None,
241 })
242}
243
244fn create_dependency_purl(dep: &SwiftDependency, version: Option<&str>) -> Option<String> {
248 let url = dep.url.as_deref()?;
249 let (namespace, name) = parse_url_namespace_and_name(url)?;
250 let mut purl = format!("pkg:swift/{}/{}", namespace, name);
251 if let Some(version) = version {
252 purl.push('@');
253 purl.push_str(version);
254 }
255 Some(purl)
256}
257
258fn create_root_purl(
261 name: Option<&str>,
262 url: Option<&str>,
263 version: Option<&str>,
264) -> Option<String> {
265 let name = name?.trim();
266 if name.is_empty() {
267 return None;
268 }
269 let (namespace, _) = parse_url_namespace_and_name(url?)?;
270
271 let mut purl = format!("pkg:swift/{}/{}", namespace, name);
272 if let Some(version) = version {
273 purl.push('@');
274 purl.push_str(version);
275 }
276 Some(purl)
277}
278
279fn normalize_version(version: Option<String>) -> Option<String> {
280 version.and_then(|v| {
281 let trimmed = v.trim();
282 if trimmed.is_empty() || trimmed == "unspecified" {
283 None
284 } else {
285 Some(trimmed.to_string())
286 }
287 })
288}
289
290fn normalize_remote_url(url: Option<String>) -> Option<String> {
291 url.and_then(|value| {
292 let trimmed = value.trim();
293 if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
294 Some(trimmed.to_string())
295 } else {
296 None
297 }
298 })
299}
300
301fn extract_namespace(url: Option<&str>) -> Option<String> {
302 parse_url_namespace_and_name(url?).map(|(namespace, _)| namespace)
303}
304
305fn parse_url_namespace_and_name(url: &str) -> Option<(String, String)> {
306 let trimmed = url.trim().trim_end_matches('/').trim_end_matches(".git");
307 let without_scheme = trimmed
308 .strip_prefix("https://")
309 .or_else(|| trimmed.strip_prefix("http://"))?;
310 let mut parts = without_scheme.split('/');
311 let host = parts.next()?;
312 let owner = parts.next()?;
313 let repo = parts.next()?;
314
315 Some((format!("{}/{}", host, owner), repo.to_string()))
316}