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