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