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