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