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