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