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