Skip to main content

provenant/parsers/
swift_show_dependencies.rs

1//! Parser for Swift show-dependencies deplock files.
2//!
3//! Extracts dependency information from `swift-show-dependencies.deplock` files
4//! created by the dependency-inspector tool.
5//!
6//! # Supported Formats
7//! - `swift-show-dependencies.deplock` - Swift dependency graph JSON
8//!
9//! # Implementation Notes
10//! - Format: JSON with nested dependency tree
11//! - Generated by deplock tool or `swift package show-dependencies`
12//! - Extracts full dependency graph with versions (beyond Python which only extracts name)
13//! - Spec: https://forums.swift.org/t/swiftpm-show-dependencies-without-fetching-dependencies/51154
14
15use std::fs;
16use std::path::Path;
17
18use log::warn;
19use serde::{Deserialize, Serialize};
20
21use crate::models::{DatasourceId, Dependency, PackageData, PackageType, ResolvedPackage};
22
23use super::PackageParser;
24
25const PACKAGE_TYPE: PackageType = PackageType::Swift;
26
27fn default_package_data() -> PackageData {
28    PackageData {
29        package_type: Some(PACKAGE_TYPE),
30        primary_language: Some("Swift".to_string()),
31        datasource_id: Some(DatasourceId::SwiftPackageShowDependencies),
32        ..Default::default()
33    }
34}
35
36pub struct SwiftShowDependenciesParser;
37
38#[derive(Debug, Deserialize, Serialize)]
39struct SwiftDeplock {
40    name: Option<String>,
41    version: Option<String>,
42    url: Option<String>,
43    #[serde(default)]
44    dependencies: Vec<SwiftDependency>,
45}
46
47#[derive(Debug, Deserialize, Serialize, Clone)]
48struct SwiftDependency {
49    identity: Option<String>,
50    name: Option<String>,
51    version: Option<String>,
52    url: Option<String>,
53    #[serde(default)]
54    dependencies: Vec<SwiftDependency>,
55}
56
57impl PackageParser for SwiftShowDependenciesParser {
58    const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
59
60    fn is_match(path: &Path) -> bool {
61        path.to_str()
62            .is_some_and(|p| p.ends_with("/swift-show-dependencies.deplock"))
63    }
64
65    fn extract_packages(path: &Path) -> Vec<PackageData> {
66        let content = match fs::read_to_string(path) {
67            Ok(c) => c,
68            Err(e) => {
69                warn!(
70                    "Failed to read swift-show-dependencies.deplock {:?}: {}",
71                    path, e
72                );
73                return vec![default_package_data()];
74            }
75        };
76
77        vec![parse_swift_show_dependencies(&content)]
78    }
79}
80
81pub(crate) fn parse_swift_show_dependencies(content: &str) -> PackageData {
82    let data: SwiftDeplock = match serde_json::from_str(content) {
83        Ok(d) => d,
84        Err(e) => {
85            warn!("Failed to parse swift-show-dependencies.deplock: {}", e);
86            return default_package_data();
87        }
88    };
89
90    let dependencies = flatten_dependencies(&data.dependencies);
91    let version = normalize_version(data.version);
92    let homepage_url = normalize_remote_url(data.url);
93    let purl = create_root_purl(data.name.as_deref(), version.as_deref());
94
95    PackageData {
96        package_type: Some(PACKAGE_TYPE),
97        primary_language: Some("Swift".to_string()),
98        name: data.name,
99        version,
100        homepage_url,
101        dependencies,
102        datasource_id: Some(DatasourceId::SwiftPackageShowDependencies),
103        purl,
104        ..Default::default()
105    }
106}
107
108fn flatten_dependencies(deps: &[SwiftDependency]) -> Vec<Dependency> {
109    let mut result = Vec::new();
110    for dep in deps {
111        flatten_dependency(dep, true, &mut result);
112    }
113
114    result
115}
116
117fn flatten_dependency(dep: &SwiftDependency, is_direct: bool, result: &mut Vec<Dependency>) {
118    if let Some(dependency) = build_dependency(dep, is_direct) {
119        result.push(dependency);
120    }
121
122    for child in &dep.dependencies {
123        flatten_dependency(child, false, result);
124    }
125}
126
127fn build_dependency(dep: &SwiftDependency, is_direct: bool) -> Option<Dependency> {
128    let name = dep.name.as_ref()?.clone();
129    let version = normalize_version(dep.version.clone());
130    let purl = create_dependency_purl(dep, &name, version.as_deref());
131    let nested_dependencies = dep
132        .dependencies
133        .iter()
134        .filter_map(|child| build_dependency(child, true))
135        .collect();
136
137    Some(Dependency {
138        purl: Some(purl.clone()),
139        extracted_requirement: version.clone(),
140        scope: Some("dependencies".to_string()),
141        is_runtime: Some(false),
142        is_optional: Some(false),
143        is_pinned: Some(version.is_some()),
144        is_direct: Some(is_direct),
145        resolved_package: Some(Box::new(ResolvedPackage {
146            package_type: PACKAGE_TYPE,
147            namespace: extract_namespace(dep.url.as_deref()).unwrap_or_default(),
148            name,
149            version: version.clone().unwrap_or_default(),
150            primary_language: Some("Swift".to_string()),
151            download_url: None,
152            sha1: None,
153            sha256: None,
154            sha512: None,
155            md5: None,
156            is_virtual: true,
157            extra_data: None,
158            dependencies: nested_dependencies,
159            repository_homepage_url: None,
160            repository_download_url: None,
161            api_data_url: None,
162            datasource_id: Some(DatasourceId::SwiftPackageShowDependencies),
163            purl: None,
164        })),
165        extra_data: None,
166    })
167}
168
169fn create_dependency_purl(
170    dep: &SwiftDependency,
171    fallback_name: &str,
172    version: Option<&str>,
173) -> String {
174    if let Some(url) = dep.url.as_deref()
175        && let Some((namespace, name)) = parse_url_namespace_and_name(url)
176    {
177        let mut purl = format!("pkg:swift/{}/{}", namespace, name);
178        if let Some(version) = version {
179            purl.push('@');
180            purl.push_str(version);
181        }
182        return purl;
183    }
184
185    let mut purl = format!("pkg:swift/{}", fallback_name);
186    if let Some(version) = version {
187        purl.push('@');
188        purl.push_str(version);
189    }
190    purl
191}
192
193fn create_root_purl(name: Option<&str>, version: Option<&str>) -> Option<String> {
194    let name = name?.trim();
195    if name.is_empty() {
196        return None;
197    }
198
199    let mut purl = format!("pkg:swift/{}", name);
200    if let Some(version) = version {
201        purl.push('@');
202        purl.push_str(version);
203    }
204    Some(purl)
205}
206
207fn normalize_version(version: Option<String>) -> Option<String> {
208    version.and_then(|v| {
209        let trimmed = v.trim();
210        if trimmed.is_empty() || trimmed == "unspecified" {
211            None
212        } else {
213            Some(trimmed.to_string())
214        }
215    })
216}
217
218fn normalize_remote_url(url: Option<String>) -> Option<String> {
219    url.and_then(|value| {
220        let trimmed = value.trim();
221        if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
222            Some(trimmed.to_string())
223        } else {
224            None
225        }
226    })
227}
228
229fn extract_namespace(url: Option<&str>) -> Option<String> {
230    parse_url_namespace_and_name(url?).map(|(namespace, _)| namespace)
231}
232
233fn parse_url_namespace_and_name(url: &str) -> Option<(String, String)> {
234    let trimmed = url.trim().trim_end_matches('/').trim_end_matches(".git");
235    let without_scheme = trimmed
236        .strip_prefix("https://")
237        .or_else(|| trimmed.strip_prefix("http://"))?;
238    let mut parts = without_scheme.split('/');
239    let host = parts.next()?;
240    let owner = parts.next()?;
241    let repo = parts.next()?;
242
243    Some((format!("{}/{}", host, owner), repo.to_string()))
244}
245
246crate::register_parser!(
247    "Swift show-dependencies deplock file",
248    &["*swift-show-dependencies.deplock"],
249    "swift",
250    "Swift",
251    Some(
252        "https://forums.swift.org/t/swiftpm-show-dependencies-without-fetching-dependencies/51154"
253    ),
254);