1use std::collections::HashMap;
7use std::path::Path;
8
9use super::utils::{capped_iteration_limit, 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: Option<String> = None;
107
108 PackageData {
109 package_type: Some(SwiftManifestJsonParser::PACKAGE_TYPE),
110 namespace: None,
111 name,
112 version: None,
113 qualifiers: None,
114 subpath: None,
115 primary_language: Some("Swift".to_string()),
116 description: None,
117 release_date: None,
118 parties: Vec::new(),
119 keywords: Vec::new(),
120 homepage_url: None,
121 download_url: None,
122 size: None,
123 sha1: None,
124 md5: None,
125 sha256: None,
126 sha512: None,
127 bug_tracking_url: None,
128 code_view_url: None,
129 vcs_url: None,
130 copyright: None,
131 holder: None,
132 declared_license_expression: None,
133 declared_license_expression_spdx: None,
134 license_detections: Vec::new(),
135 other_license_expression: None,
136 other_license_expression_spdx: None,
137 other_license_detections: Vec::new(),
138 extracted_license_statement: None,
139 notice_text: None,
140 source_packages: Vec::new(),
141 file_references: Vec::new(),
142 is_private: false,
143 is_virtual: false,
144 extra_data: if extra_data.is_empty() {
145 None
146 } else {
147 Some(extra_data)
148 },
149 dependencies,
150 repository_homepage_url: None,
151 repository_download_url: None,
152 api_data_url: None,
153 datasource_id: Some(DatasourceId::SwiftPackageManifestJson),
154 purl,
155 }
156}
157
158fn get_dependencies(dependencies: Option<&Value>) -> Vec<Dependency> {
159 let Some(deps_array) = dependencies.and_then(|v| v.as_array()) else {
160 return Vec::new();
161 };
162
163 let mut dependent_packages = Vec::new();
164
165 let limit = capped_iteration_limit(deps_array.len(), "Swift manifest dependencies");
166 for dependency in deps_array.iter().take(limit) {
167 if let Some(dep) = parse_manifest_dependency(dependency) {
168 dependent_packages.push(dep);
169 }
170 }
171
172 dependent_packages
173}
174
175fn parse_manifest_dependency(dependency: &Value) -> Option<Dependency> {
176 if let Some(source_control) = dependency.get("sourceControl").and_then(|v| v.as_array())
177 && let Some(source) = source_control.first()
178 {
179 let identity = source
180 .get("identity")
181 .and_then(|v| v.as_str())
182 .unwrap_or_default();
183
184 let (mut namespace, mut dep_name) = extract_namespace_and_name(source, identity);
185 namespace = namespace.map(truncate_field);
186 dep_name = truncate_field(dep_name);
187 let (version, is_pinned, requirement_kind) = extract_version_requirement(source);
188 let version = version.map(truncate_field);
189 let purl =
190 create_dependency_purl(&namespace, &dep_name, &version, is_pinned).map(truncate_field);
191 let mut extra_data = HashMap::from([
192 (
193 "dependency_kind".to_string(),
194 serde_json::Value::String("sourceControl".to_string()),
195 ),
196 (
197 "requirement_kind".to_string(),
198 serde_json::Value::String(requirement_kind.to_string()),
199 ),
200 ]);
201 if let Some(remote) = source
202 .get("location")
203 .and_then(|loc| loc.get("remote"))
204 .and_then(|remote| remote.as_array())
205 .and_then(|arr| arr.first())
206 .and_then(|first| first.get("urlString"))
207 .and_then(|v| v.as_str())
208 {
209 extra_data.insert(
210 "location".to_string(),
211 serde_json::Value::String(remote.to_string()),
212 );
213 }
214
215 return Some(Dependency {
216 purl,
217 extracted_requirement: version,
218 scope: Some("dependencies".to_string()),
219 is_runtime: None,
220 is_optional: Some(false),
221 is_pinned: Some(is_pinned),
222 is_direct: Some(true),
223 resolved_package: None,
224 extra_data: Some(extra_data),
225 });
226 }
227
228 if let Some(file_system) = dependency.get("fileSystem").and_then(|v| v.as_array())
229 && let Some(source) = file_system.first()
230 {
231 let identity = source
232 .get("identity")
233 .and_then(|v| v.as_str())
234 .or_else(|| source.get("name").and_then(|v| v.as_str()))
235 .unwrap_or_default();
236 if identity.is_empty() {
237 return None;
238 }
239
240 let dep_name = truncate_field(identity.to_string());
241 let purl = create_dependency_purl(&None, &dep_name, &None, false).map(truncate_field);
242 let mut extra_data = HashMap::from([(
243 "dependency_kind".to_string(),
244 serde_json::Value::String("fileSystem".to_string()),
245 )]);
246 if let Some(path) = source.get("path").and_then(|v| v.as_str()) {
247 extra_data.insert(
248 "path".to_string(),
249 serde_json::Value::String(path.to_string()),
250 );
251 }
252
253 return Some(Dependency {
254 purl,
255 extracted_requirement: None,
256 scope: Some("dependencies".to_string()),
257 is_runtime: None,
258 is_optional: Some(false),
259 is_pinned: Some(false),
260 is_direct: Some(true),
261 resolved_package: None,
262 extra_data: Some(extra_data),
263 });
264 }
265
266 None
267}
268
269fn extract_namespace_and_name(source: &Value, identity: &str) -> (Option<String>, String) {
270 let url = source
271 .get("location")
272 .and_then(|loc| loc.get("remote"))
273 .and_then(|remote| remote.as_array())
274 .and_then(|arr| arr.first())
275 .and_then(|first| first.get("urlString"))
276 .and_then(|v| v.as_str());
277
278 match url {
279 Some(url_str) => get_namespace_and_name(url_str),
280 None => (None, identity.to_string()),
281 }
282}
283
284pub fn get_namespace_and_name(url: &str) -> (Option<String>, String) {
289 let (hostname, path) = if let Some(stripped) = url.strip_prefix("https://") {
290 let rest = stripped.trim_end_matches('/');
291 match rest.find('/') {
292 Some(idx) => (Some(&rest[..idx]), &rest[idx + 1..]),
293 None => (Some(rest), ""),
294 }
295 } else if let Some(stripped) = url.strip_prefix("http://") {
296 let rest = stripped.trim_end_matches('/');
297 match rest.find('/') {
298 Some(idx) => (Some(&rest[..idx]), &rest[idx + 1..]),
299 None => (Some(rest), ""),
300 }
301 } else {
302 (None, url)
303 };
304
305 let clean_path = path
306 .strip_suffix(".git")
307 .unwrap_or(path)
308 .trim_end_matches('/');
309
310 if let Some(host) = hostname {
311 let canonical = format!("{}/{}", host, clean_path);
312 match canonical.rsplit_once('/') {
313 Some((ns, name)) => (Some(ns.to_string()), name.to_string()),
314 None => (None, canonical),
315 }
316 } else {
317 match clean_path.rsplit_once('/') {
318 Some((ns, name)) => (Some(ns.to_string()), name.to_string()),
319 None => (None, clean_path.to_string()),
320 }
321 }
322}
323
324fn extract_version_requirement(source: &Value) -> (Option<String>, bool, &'static str) {
330 let Some(requirement) = source.get("requirement") else {
331 return (None, false, "unknown");
332 };
333
334 if let Some(exact) = requirement.get("exact").and_then(|v| v.as_array())
335 && let Some(version) = exact.first().and_then(|v| v.as_str())
336 {
337 return (Some(version.to_string()), true, "exact");
338 }
339
340 if let Some(range) = requirement.get("range").and_then(|v| v.as_array())
341 && let Some(bound) = range.first()
342 {
343 let lower = bound.get("lowerBound").and_then(|v| v.as_str());
344 let upper = bound.get("upperBound").and_then(|v| v.as_str());
345 if let (Some(lb), Some(ub)) = (lower, upper) {
346 let vers = format!("vers:swift/>={lb}|<{ub}");
347 return (Some(vers), false, "range");
348 }
349 }
350
351 if let Some(branch) = requirement.get("branch").and_then(|v| v.as_array())
352 && let Some(branch_name) = branch.first().and_then(|v| v.as_str())
353 {
354 return (Some(branch_name.to_string()), false, "branch");
355 }
356
357 if let Some(revision) = requirement.get("revision").and_then(|v| v.as_array())
358 && let Some(rev) = revision.first().and_then(|v| v.as_str())
359 {
360 return (Some(rev.to_string()), true, "revision");
361 }
362
363 (None, false, "unknown")
364}
365
366fn create_dependency_purl(
367 namespace: &Option<String>,
368 name: &str,
369 version: &Option<String>,
370 is_pinned: bool,
371) -> Option<String> {
372 let namespace = namespace.as_deref()?;
376 let mut purl = PackageUrl::new(SwiftManifestJsonParser::PACKAGE_TYPE.as_str(), name).ok()?;
377 purl.with_namespace(namespace).ok()?;
378
379 if is_pinned
380 && let Some(v) = version
381 && let Err(e) = purl.with_version(v)
382 {
383 warn!(
384 "Failed to set version '{}' for swift dependency '{}': {}",
385 v, name, e
386 );
387 }
388
389 Some(purl.to_string())
390}
391
392fn default_package_data(path: &Path) -> PackageData {
393 let _ = path;
394
395 PackageData {
396 package_type: Some(SwiftManifestJsonParser::PACKAGE_TYPE),
397 primary_language: Some("Swift".to_string()),
398 datasource_id: Some(DatasourceId::SwiftPackageManifestJson),
399 ..Default::default()
400 }
401}