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