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