Skip to main content

package_parser/pkgs/
swift.rs

1use std::{fs::File, path::Path};
2
3use crate::types::{DependentPackage, Package};
4use serde_json::Value;
5
6use crate::{error::SourcePkgError, PackageManifest};
7
8mod v1 {
9    use serde::Deserialize;
10
11    #[derive(Debug, Deserialize)]
12    pub struct LockFile {
13        pub object: Object,
14    }
15
16    #[derive(Debug, Deserialize)]
17    pub struct Object {
18        #[serde(default)]
19        pub pins: Vec<PinnedPackage>,
20    }
21
22    #[derive(Debug, Deserialize)]
23    pub struct PinnedPackage {
24        pub package: String,
25        #[serde(rename = "repositoryURL")]
26        pub repository_url: Option<String>,
27        pub state: PinnedPackageState,
28    }
29
30    #[derive(Debug, Deserialize)]
31    #[serde(rename_all = "camelCase")]
32    pub struct PinnedPackageState {
33        pub version: Option<String>,
34        // pub branch: Option<String>,
35        pub revision: Option<String>,
36    }
37}
38
39mod v2 {
40    use serde::Deserialize;
41
42    #[derive(Debug, Deserialize)]
43    pub struct LockFile {
44        pub pins: Vec<PinnedPackage>,
45    }
46
47    #[derive(Debug, Deserialize)]
48    #[allow(unused)]
49    pub struct PinnedPackage {
50        pub location: String,
51        pub state: PinnedPackageState,
52    }
53
54    #[derive(Debug, Deserialize)]
55    #[allow(unused)]
56    pub struct PinnedPackageState {
57        pub version: Option<String>,
58        pub revision: Option<String>,
59    }
60}
61
62fn repo_url_to_purl(url: &str) -> Option<String> {
63    let url = if let Some(url) = url.split_once("://") {
64        url.1
65    } else {
66        return None;
67    };
68
69    let url = url.strip_suffix(".git").unwrap_or(url);
70
71    Some(format!("pkg:swift/{}", url))
72}
73
74pub struct SwiftPmLock;
75
76impl SwiftPmLock {
77    pub fn new() -> Self {
78        Self
79    }
80
81    fn parse(path: &Path) -> Result<Package, SourcePkgError> {
82        let file = File::open(path)?;
83        let resolved: Value = serde_json::from_reader(file)?;
84
85        let mut deps = vec![];
86
87        let version = resolved.get("version").and_then(|v| v.as_i64());
88        match version {
89            Some(1) => {
90                let resolved = serde_json::from_value::<v1::LockFile>(resolved)?;
91                for pkg in resolved.object.pins {
92                    let url = if let Some(url) = pkg.repository_url {
93                        url
94                    } else {
95                        log::warn!("No URL for package {}", pkg.package);
96                        continue;
97                    };
98
99                    let mut purl = if let Some(purl) = repo_url_to_purl(&url) {
100                        purl
101                    } else {
102                        log::warn!("Could not convert URL {} to purl", url);
103                        continue;
104                    };
105
106                    let version = pkg.state.version.or(pkg.state.revision);
107
108                    if let Some(v) = &version {
109                        purl.push_str(&format!("@{}", v));
110                    }
111
112                    let dep = DependentPackage {
113                        purl,
114                        requirement: version.unwrap_or_default(),
115                        scope: "".into(),
116                        is_runtime: true,
117                        is_optional: false,
118                        is_resolved: true,
119                        ..Default::default()
120                    };
121
122                    deps.push(dep);
123                }
124            }
125            Some(2) => {
126                let resolved = serde_json::from_value::<v2::LockFile>(resolved)?;
127
128                for pkg in resolved.pins {
129                    let url = pkg.location;
130
131                    let mut purl = if let Some(purl) = repo_url_to_purl(&url) {
132                        purl
133                    } else {
134                        log::warn!("Could not convert URL {} to purl", url);
135                        continue;
136                    };
137
138                    let version = pkg.state.version.or(pkg.state.revision);
139
140                    if let Some(v) = &version {
141                        purl.push_str(&format!("@{}", v));
142                    }
143
144                    let dep = DependentPackage {
145                        purl,
146                        requirement: version.unwrap_or_default(),
147                        scope: "".into(),
148                        is_runtime: true,
149                        is_optional: false,
150                        is_resolved: true,
151                        ..Default::default()
152                    };
153
154                    deps.push(dep);
155                }
156            }
157            v => {
158                return Err(SourcePkgError::GenericsError2(format!(
159                    "Unrecognized swift package manager lock file version: {:?}",
160                    v
161                )));
162            }
163        }
164
165        Ok(Package {
166            dependencies: deps,
167            ..Default::default()
168        })
169    }
170}
171
172#[async_trait::async_trait]
173impl PackageManifest for SwiftPmLock {
174    fn get_name(&self) -> String {
175        "swift".to_string()
176    }
177
178    async fn recognize(&self, path: &Path) -> Result<Package, SourcePkgError> {
179        Self::parse(path)
180    }
181
182    fn file_name_patterns(&self) -> &'static [&'static str] {
183        &["Package.resolved"]
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    #[test]
192    fn v1() {
193        let filepath = Path::new(concat!(
194            env!("CARGO_MANIFEST_DIR"),
195            "/testdata/swift/v1_erik.resolved"
196        ));
197
198        let p = SwiftPmLock::parse(filepath).unwrap();
199        println!("{:?}", p);
200    }
201
202    #[test]
203    fn v2() {
204        let filepath = Path::new(concat!(
205            env!("CARGO_MANIFEST_DIR"),
206            "/testdata/swift/v2_firefox.resolved"
207        ));
208
209        let p = SwiftPmLock::parse(filepath).unwrap();
210        println!("{:?}", p);
211    }
212}