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 primary_language: Some("Swift".to_string()),
148 download_url: None,
149 sha1: None,
150 sha256: None,
151 sha512: None,
152 md5: None,
153 is_virtual: true,
154 extra_data: None,
155 dependencies: nested_dependencies,
156 repository_homepage_url: None,
157 repository_download_url: None,
158 api_data_url: None,
159 datasource_id: Some(DatasourceId::SwiftPackageShowDependencies),
160 purl: None,
161 ..ResolvedPackage::new(
162 PACKAGE_TYPE,
163 extract_namespace(dep.url.as_deref()).unwrap_or_default(),
164 name,
165 version.clone().unwrap_or_default(),
166 )
167 })),
168 extra_data: None,
169 })
170}
171
172fn create_dependency_purl(
173 dep: &SwiftDependency,
174 fallback_name: &str,
175 version: Option<&str>,
176) -> String {
177 if let Some(url) = dep.url.as_deref()
178 && let Some((namespace, name)) = parse_url_namespace_and_name(url)
179 {
180 let mut purl = format!("pkg:swift/{}/{}", namespace, name);
181 if let Some(version) = version {
182 purl.push('@');
183 purl.push_str(version);
184 }
185 return purl;
186 }
187
188 let mut purl = format!("pkg:swift/{}", fallback_name);
189 if let Some(version) = version {
190 purl.push('@');
191 purl.push_str(version);
192 }
193 purl
194}
195
196fn create_root_purl(name: Option<&str>, version: Option<&str>) -> Option<String> {
197 let name = name?.trim();
198 if name.is_empty() {
199 return None;
200 }
201
202 let mut purl = format!("pkg:swift/{}", name);
203 if let Some(version) = version {
204 purl.push('@');
205 purl.push_str(version);
206 }
207 Some(purl)
208}
209
210fn normalize_version(version: Option<String>) -> Option<String> {
211 version.and_then(|v| {
212 let trimmed = v.trim();
213 if trimmed.is_empty() || trimmed == "unspecified" {
214 None
215 } else {
216 Some(trimmed.to_string())
217 }
218 })
219}
220
221fn normalize_remote_url(url: Option<String>) -> Option<String> {
222 url.and_then(|value| {
223 let trimmed = value.trim();
224 if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
225 Some(trimmed.to_string())
226 } else {
227 None
228 }
229 })
230}
231
232fn extract_namespace(url: Option<&str>) -> Option<String> {
233 parse_url_namespace_and_name(url?).map(|(namespace, _)| namespace)
234}
235
236fn parse_url_namespace_and_name(url: &str) -> Option<(String, String)> {
237 let trimmed = url.trim().trim_end_matches('/').trim_end_matches(".git");
238 let without_scheme = trimmed
239 .strip_prefix("https://")
240 .or_else(|| trimmed.strip_prefix("http://"))?;
241 let mut parts = without_scheme.split('/');
242 let host = parts.next()?;
243 let owner = parts.next()?;
244 let repo = parts.next()?;
245
246 Some((format!("{}/{}", host, owner), repo.to_string()))
247}
248
249crate::register_parser!(
250 "Swift show-dependencies deplock file",
251 &["*swift-show-dependencies.deplock"],
252 "swift",
253 "Swift",
254 Some(
255 "https://forums.swift.org/t/swiftpm-show-dependencies-without-fetching-dependencies/51154"
256 ),
257);