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 fn metadata() -> Vec<super::metadata::ParserMetadata> {
87 vec![super::metadata::ParserMetadata {
88 description: "Swift show-dependencies deplock file",
89 file_patterns: &["*swift-show-dependencies.deplock"],
90 package_type: "swift",
91 primary_language: "Swift",
92 documentation_url: Some(
93 "https://forums.swift.org/t/swiftpm-show-dependencies-without-fetching-dependencies/51154",
94 ),
95 }]
96 }
97}
98
99pub(crate) fn parse_swift_show_dependencies(content: &str) -> PackageData {
100 let data: SwiftDeplock = match serde_json::from_str(content) {
101 Ok(d) => d,
102 Err(e) => {
103 warn!("Failed to parse swift-show-dependencies.deplock: {}", e);
104 return default_package_data();
105 }
106 };
107
108 let dependencies = flatten_dependencies(&data.dependencies);
109 let version = normalize_version(data.version);
110 let homepage_url = normalize_remote_url(data.url);
111 let purl = create_root_purl(data.name.as_deref(), version.as_deref());
112
113 PackageData {
114 package_type: Some(PACKAGE_TYPE),
115 primary_language: Some("Swift".to_string()),
116 name: data.name.map(truncate_field),
117 version: version.map(truncate_field),
118 homepage_url: homepage_url.map(truncate_field),
119 dependencies,
120 datasource_id: Some(DatasourceId::SwiftPackageShowDependencies),
121 purl,
122 ..Default::default()
123 }
124}
125
126fn flatten_dependencies(deps: &[SwiftDependency]) -> Vec<Dependency> {
127 let mut result = Vec::new();
128
129 for dep in deps {
130 let mut guard: RecursionGuard<String> = RecursionGuard::new();
131 let mut depth_guard = RecursionGuard::<()>::depth_only();
132 flatten_dependency(dep, true, &mut result, &mut guard, &mut depth_guard);
133 }
134
135 result
136}
137
138fn flatten_dependency(
139 dep: &SwiftDependency,
140 is_direct: bool,
141 result: &mut Vec<Dependency>,
142 guard: &mut RecursionGuard<String>,
143 depth_guard: &mut RecursionGuard<()>,
144) {
145 if guard.exceeded() || depth_guard.exceeded() {
146 warn!("Recursion depth exceeded in swift dependency flattening");
147 return;
148 }
149
150 if result.len() >= MAX_ITERATION_COUNT {
151 return;
152 }
153
154 let dep_key = dep
155 .identity
156 .as_deref()
157 .or(dep.name.as_deref())
158 .unwrap_or("")
159 .to_string();
160 if !dep_key.is_empty() && guard.enter(dep_key.clone()) {
161 return;
162 }
163
164 if let Some(dependency) = build_dependency(dep, is_direct, depth_guard) {
165 result.push(dependency);
166 }
167
168 for child in &dep.dependencies {
169 flatten_dependency(child, false, result, guard, depth_guard);
170 }
171
172 if !dep_key.is_empty() {
173 guard.leave(dep_key);
174 }
175}
176
177fn build_dependency(
178 dep: &SwiftDependency,
179 is_direct: bool,
180 guard: &mut RecursionGuard<()>,
181) -> Option<Dependency> {
182 if guard.descend() {
183 warn!("Recursion depth exceeded in swift dependency building");
184 return None;
185 }
186
187 let name = truncate_field(dep.name.as_ref()?.clone());
188 let version = normalize_version(dep.version.clone());
189 let purl = create_dependency_purl(dep, &name, version.as_deref());
190 let nested_dependencies = dep
191 .dependencies
192 .iter()
193 .take(MAX_ITERATION_COUNT)
194 .filter_map(|child| build_dependency(child, true, guard))
195 .collect();
196
197 guard.ascend();
198
199 Some(Dependency {
200 purl: Some(truncate_field(purl.clone())),
201 extracted_requirement: version.clone().map(truncate_field),
202 scope: Some("dependencies".to_string()),
203 is_runtime: None,
204 is_optional: None,
205 is_pinned: Some(version.is_some()),
206 is_direct: Some(is_direct),
207 resolved_package: Some(Box::new(ResolvedPackage {
208 primary_language: Some("Swift".to_string()),
209 download_url: None,
210 sha1: None,
211 sha256: None,
212 sha512: None,
213 md5: None,
214 is_virtual: true,
215 extra_data: None,
216 dependencies: nested_dependencies,
217 repository_homepage_url: None,
218 repository_download_url: None,
219 api_data_url: None,
220 datasource_id: Some(DatasourceId::SwiftPackageShowDependencies),
221 purl: None,
222 ..ResolvedPackage::new(
223 PACKAGE_TYPE,
224 truncate_field(extract_namespace(dep.url.as_deref()).unwrap_or_default()),
225 name,
226 truncate_field(version.clone().unwrap_or_default()),
227 )
228 })),
229 extra_data: None,
230 })
231}
232
233fn create_dependency_purl(
234 dep: &SwiftDependency,
235 fallback_name: &str,
236 version: Option<&str>,
237) -> String {
238 if let Some(url) = dep.url.as_deref()
239 && let Some((namespace, name)) = parse_url_namespace_and_name(url)
240 {
241 let mut purl = format!("pkg:swift/{}/{}", namespace, name);
242 if let Some(version) = version {
243 purl.push('@');
244 purl.push_str(version);
245 }
246 return purl;
247 }
248
249 let mut purl = format!("pkg:swift/{}", fallback_name);
250 if let Some(version) = version {
251 purl.push('@');
252 purl.push_str(version);
253 }
254 purl
255}
256
257fn create_root_purl(name: Option<&str>, version: Option<&str>) -> Option<String> {
258 let name = name?.trim();
259 if name.is_empty() {
260 return None;
261 }
262
263 let mut purl = format!("pkg:swift/{}", name);
264 if let Some(version) = version {
265 purl.push('@');
266 purl.push_str(version);
267 }
268 Some(purl)
269}
270
271fn normalize_version(version: Option<String>) -> Option<String> {
272 version.and_then(|v| {
273 let trimmed = v.trim();
274 if trimmed.is_empty() || trimmed == "unspecified" {
275 None
276 } else {
277 Some(trimmed.to_string())
278 }
279 })
280}
281
282fn normalize_remote_url(url: Option<String>) -> Option<String> {
283 url.and_then(|value| {
284 let trimmed = value.trim();
285 if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
286 Some(trimmed.to_string())
287 } else {
288 None
289 }
290 })
291}
292
293fn extract_namespace(url: Option<&str>) -> Option<String> {
294 parse_url_namespace_and_name(url?).map(|(namespace, _)| namespace)
295}
296
297fn parse_url_namespace_and_name(url: &str) -> Option<(String, String)> {
298 let trimmed = url.trim().trim_end_matches('/').trim_end_matches(".git");
299 let without_scheme = trimmed
300 .strip_prefix("https://")
301 .or_else(|| trimmed.strip_prefix("http://"))?;
302 let mut parts = without_scheme.split('/');
303 let host = parts.next()?;
304 let owner = parts.next()?;
305 let repo = parts.next()?;
306
307 Some((format!("{}/{}", host, owner), repo.to_string()))
308}