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