1use std::collections::HashMap;
2use std::fs;
3use std::path::Path;
4
5use crate::parser_warn as warn;
6use packageurl::PackageUrl;
7use serde_json::Value;
8
9use crate::models::{DatasourceId, Dependency, PackageData, PackageType};
10
11use super::PackageParser;
12
13pub struct SwiftManifestJsonParser;
18
19impl PackageParser for SwiftManifestJsonParser {
20 const PACKAGE_TYPE: PackageType = PackageType::Swift;
21
22 fn extract_packages(path: &Path) -> Vec<PackageData> {
23 let filename = path.file_name().and_then(|n| n.to_str());
24
25 vec![if filename
26 .map(|n| n.ends_with(".swift.json") || n.ends_with(".swift.deplock"))
27 .unwrap_or(false)
28 {
29 let json_content = match read_swift_manifest_json(path) {
30 Ok(content) => content,
31 Err(e) => {
32 warn!(
33 "Failed to read or parse Swift manifest JSON at {:?}: {}",
34 path, e
35 );
36 return vec![default_package_data(path)];
37 }
38 };
39 parse_swift_manifest(&json_content)
40 } else {
41 default_package_data(path)
42 }]
43 }
44
45 fn is_match(path: &Path) -> bool {
46 path.file_name()
47 .and_then(|name| name.to_str())
48 .is_some_and(|name| name.ends_with(".swift.json") || name.ends_with(".swift.deplock"))
49 }
50}
51
52fn read_swift_manifest_json(path: &Path) -> Result<Value, String> {
53 let content = fs::read_to_string(path).map_err(|e| format!("Failed to read file: {}", e))?;
54
55 serde_json::from_str(&content).map_err(|e| format!("Failed to parse JSON: {}", e))
56}
57
58fn parse_swift_manifest(manifest: &Value) -> PackageData {
59 let name = manifest
60 .get("name")
61 .and_then(|v| v.as_str())
62 .map(String::from);
63
64 let dependencies = get_dependencies(manifest.get("dependencies"));
65 let platforms = manifest.get("platforms").cloned();
66
67 let tools_version = manifest
68 .get("toolsVersion")
69 .and_then(|tv| tv.get("_version"))
70 .and_then(|v| v.as_str())
71 .map(String::from);
72
73 let mut extra_data = HashMap::new();
74 if let Some(platforms_val) = platforms {
75 extra_data.insert("platforms".to_string(), platforms_val);
76 }
77 if let Some(ref tv) = tools_version {
78 extra_data.insert(
79 "swift_tools_version".to_string(),
80 serde_json::Value::String(tv.clone()),
81 );
82 }
83
84 let purl = create_package_url(&name, &None);
85
86 PackageData {
87 package_type: Some(SwiftManifestJsonParser::PACKAGE_TYPE),
88 namespace: None,
89 name,
90 version: None,
91 qualifiers: None,
92 subpath: None,
93 primary_language: Some("Swift".to_string()),
94 description: None,
95 release_date: None,
96 parties: Vec::new(),
97 keywords: Vec::new(),
98 homepage_url: None,
99 download_url: None,
100 size: None,
101 sha1: None,
102 md5: None,
103 sha256: None,
104 sha512: None,
105 bug_tracking_url: None,
106 code_view_url: None,
107 vcs_url: None,
108 copyright: None,
109 holder: None,
110 declared_license_expression: None,
111 declared_license_expression_spdx: None,
112 license_detections: Vec::new(),
113 other_license_expression: None,
114 other_license_expression_spdx: None,
115 other_license_detections: Vec::new(),
116 extracted_license_statement: None,
117 notice_text: None,
118 source_packages: Vec::new(),
119 file_references: Vec::new(),
120 is_private: false,
121 is_virtual: false,
122 extra_data: if extra_data.is_empty() {
123 None
124 } else {
125 Some(extra_data)
126 },
127 dependencies,
128 repository_homepage_url: None,
129 repository_download_url: None,
130 api_data_url: None,
131 datasource_id: Some(DatasourceId::SwiftPackageManifestJson),
132 purl,
133 }
134}
135
136fn get_dependencies(dependencies: Option<&Value>) -> Vec<Dependency> {
137 let Some(deps_array) = dependencies.and_then(|v| v.as_array()) else {
138 return Vec::new();
139 };
140
141 let mut dependent_packages = Vec::new();
142
143 for dependency in deps_array {
144 if let Some(dep) = parse_manifest_dependency(dependency) {
145 dependent_packages.push(dep);
146 }
147 }
148
149 dependent_packages
150}
151
152fn parse_manifest_dependency(dependency: &Value) -> Option<Dependency> {
153 if let Some(source_control) = dependency.get("sourceControl").and_then(|v| v.as_array())
154 && let Some(source) = source_control.first()
155 {
156 let identity = source
157 .get("identity")
158 .and_then(|v| v.as_str())
159 .unwrap_or_default();
160
161 let (namespace, dep_name) = extract_namespace_and_name(source, identity);
162 let (version, is_pinned, requirement_kind) = extract_version_requirement(source);
163 let purl = create_dependency_purl(&namespace, &dep_name, &version, is_pinned);
164 let mut extra_data = HashMap::from([
165 (
166 "dependency_kind".to_string(),
167 serde_json::Value::String("sourceControl".to_string()),
168 ),
169 (
170 "requirement_kind".to_string(),
171 serde_json::Value::String(requirement_kind.to_string()),
172 ),
173 ]);
174 if let Some(remote) = source
175 .get("location")
176 .and_then(|loc| loc.get("remote"))
177 .and_then(|remote| remote.as_array())
178 .and_then(|arr| arr.first())
179 .and_then(|first| first.get("urlString"))
180 .and_then(|v| v.as_str())
181 {
182 extra_data.insert(
183 "location".to_string(),
184 serde_json::Value::String(remote.to_string()),
185 );
186 }
187
188 return Some(Dependency {
189 purl: Some(purl),
190 extracted_requirement: version,
191 scope: Some("dependencies".to_string()),
192 is_runtime: None,
193 is_optional: Some(false),
194 is_pinned: Some(is_pinned),
195 is_direct: Some(true),
196 resolved_package: None,
197 extra_data: Some(extra_data),
198 });
199 }
200
201 if let Some(file_system) = dependency.get("fileSystem").and_then(|v| v.as_array())
202 && let Some(source) = file_system.first()
203 {
204 let identity = source
205 .get("identity")
206 .and_then(|v| v.as_str())
207 .or_else(|| source.get("name").and_then(|v| v.as_str()))
208 .unwrap_or_default();
209 if identity.is_empty() {
210 return None;
211 }
212
213 let dep_name = identity.to_string();
214 let purl = create_dependency_purl(&None, &dep_name, &None, false);
215 let mut extra_data = HashMap::from([(
216 "dependency_kind".to_string(),
217 serde_json::Value::String("fileSystem".to_string()),
218 )]);
219 if let Some(path) = source.get("path").and_then(|v| v.as_str()) {
220 extra_data.insert(
221 "path".to_string(),
222 serde_json::Value::String(path.to_string()),
223 );
224 }
225
226 return Some(Dependency {
227 purl: Some(purl),
228 extracted_requirement: None,
229 scope: Some("dependencies".to_string()),
230 is_runtime: None,
231 is_optional: Some(false),
232 is_pinned: Some(false),
233 is_direct: Some(true),
234 resolved_package: None,
235 extra_data: Some(extra_data),
236 });
237 }
238
239 None
240}
241
242fn extract_namespace_and_name(source: &Value, identity: &str) -> (Option<String>, String) {
243 let url = source
244 .get("location")
245 .and_then(|loc| loc.get("remote"))
246 .and_then(|remote| remote.as_array())
247 .and_then(|arr| arr.first())
248 .and_then(|first| first.get("urlString"))
249 .and_then(|v| v.as_str());
250
251 match url {
252 Some(url_str) => get_namespace_and_name(url_str),
253 None => (None, identity.to_string()),
254 }
255}
256
257pub fn get_namespace_and_name(url: &str) -> (Option<String>, String) {
262 let (hostname, path) = if let Some(stripped) = url.strip_prefix("https://") {
263 let rest = stripped.trim_end_matches('/');
264 match rest.find('/') {
265 Some(idx) => (Some(&rest[..idx]), &rest[idx + 1..]),
266 None => (Some(rest), ""),
267 }
268 } else if let Some(stripped) = url.strip_prefix("http://") {
269 let rest = stripped.trim_end_matches('/');
270 match rest.find('/') {
271 Some(idx) => (Some(&rest[..idx]), &rest[idx + 1..]),
272 None => (Some(rest), ""),
273 }
274 } else {
275 (None, url)
276 };
277
278 let clean_path = path
279 .strip_suffix(".git")
280 .unwrap_or(path)
281 .trim_end_matches('/');
282
283 if let Some(host) = hostname {
284 let canonical = format!("{}/{}", host, clean_path);
285 match canonical.rsplit_once('/') {
286 Some((ns, name)) => (Some(ns.to_string()), name.to_string()),
287 None => (None, canonical),
288 }
289 } else {
290 match clean_path.rsplit_once('/') {
291 Some((ns, name)) => (Some(ns.to_string()), name.to_string()),
292 None => (None, clean_path.to_string()),
293 }
294 }
295}
296
297fn extract_version_requirement(source: &Value) -> (Option<String>, bool, &'static str) {
303 let Some(requirement) = source.get("requirement") else {
304 return (None, false, "unknown");
305 };
306
307 if let Some(exact) = requirement.get("exact").and_then(|v| v.as_array())
308 && let Some(version) = exact.first().and_then(|v| v.as_str())
309 {
310 return (Some(version.to_string()), true, "exact");
311 }
312
313 if let Some(range) = requirement.get("range").and_then(|v| v.as_array())
314 && let Some(bound) = range.first()
315 {
316 let lower = bound.get("lowerBound").and_then(|v| v.as_str());
317 let upper = bound.get("upperBound").and_then(|v| v.as_str());
318 if let (Some(lb), Some(ub)) = (lower, upper) {
319 let vers = format!("vers:swift/>={lb}|<{ub}");
320 return (Some(vers), false, "range");
321 }
322 }
323
324 if let Some(branch) = requirement.get("branch").and_then(|v| v.as_array())
325 && let Some(branch_name) = branch.first().and_then(|v| v.as_str())
326 {
327 return (Some(branch_name.to_string()), false, "branch");
328 }
329
330 if let Some(revision) = requirement.get("revision").and_then(|v| v.as_array())
331 && let Some(rev) = revision.first().and_then(|v| v.as_str())
332 {
333 return (Some(rev.to_string()), true, "revision");
334 }
335
336 (None, false, "unknown")
337}
338
339fn create_dependency_purl(
340 namespace: &Option<String>,
341 name: &str,
342 version: &Option<String>,
343 is_pinned: bool,
344) -> String {
345 let mut purl = match PackageUrl::new(SwiftManifestJsonParser::PACKAGE_TYPE.as_str(), name) {
346 Ok(p) => p,
347 Err(e) => {
348 warn!(
349 "Failed to create PackageUrl for swift dependency '{}': {}",
350 name, e
351 );
352 return match (namespace, is_pinned.then_some(version.as_deref()).flatten()) {
353 (Some(ns), Some(v)) => format!("pkg:swift/{}/{}@{}", ns, name, v),
354 (Some(ns), None) => format!("pkg:swift/{}/{}", ns, name),
355 (None, Some(v)) => format!("pkg:swift/{}@{}", name, v),
356 (None, None) => format!("pkg:swift/{}", name),
357 };
358 }
359 };
360
361 if let Some(ns) = namespace
362 && let Err(e) = purl.with_namespace(ns)
363 {
364 warn!(
365 "Failed to set namespace '{}' for swift dependency '{}': {}",
366 ns, name, e
367 );
368 }
369
370 if is_pinned
371 && let Some(v) = version
372 && let Err(e) = purl.with_version(v)
373 {
374 warn!(
375 "Failed to set version '{}' for swift dependency '{}': {}",
376 v, name, e
377 );
378 }
379
380 purl.to_string()
381}
382
383fn create_package_url(name: &Option<String>, version: &Option<String>) -> Option<String> {
384 name.as_ref().and_then(|name| {
385 let mut package_url =
386 match PackageUrl::new(SwiftManifestJsonParser::PACKAGE_TYPE.as_str(), name) {
387 Ok(p) => p,
388 Err(e) => {
389 warn!(
390 "Failed to create PackageUrl for swift package '{}': {}",
391 name, e
392 );
393 return None;
394 }
395 };
396
397 if let Some(v) = version
398 && let Err(e) = package_url.with_version(v)
399 {
400 warn!(
401 "Failed to set version '{}' for swift package '{}': {}",
402 v, name, e
403 );
404 return None;
405 }
406
407 Some(package_url.to_string())
408 })
409}
410
411fn default_package_data(path: &Path) -> PackageData {
412 let _ = path;
413
414 PackageData {
415 package_type: Some(SwiftManifestJsonParser::PACKAGE_TYPE),
416 primary_language: Some("Swift".to_string()),
417 datasource_id: Some(DatasourceId::SwiftPackageManifestJson),
418 ..Default::default()
419 }
420}
421
422crate::register_parser!(
423 "Swift Package Manager manifest JSON (Package.swift.json, Package.swift.deplock)",
424 &["**/Package.swift.json", "**/Package.swift.deplock"],
425 "swift",
426 "Swift",
427 Some("https://docs.swift.org/package-manager/PackageDescription/PackageDescription.html"),
428);