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