Skip to main content

package_parser/pkgs/dotnet/
nuspec.rs

1use packageurl::PackageUrl;
2use quick_xml::de::from_str;
3use serde::{Deserialize, Serialize};
4
5use crate::error::SourcePkgError;
6use crate::pkgs::common::model::{DependentPackage, Package, PackageManifest};
7
8use std::fs::File;
9use std::io::Read;
10use std::path::Path;
11
12#[derive(Clone, Debug, Serialize, Deserialize)]
13#[serde(rename = "package")]
14struct NuSpec {
15    pub metadata: NuSpecMetadata,
16    // #[serde(default)]
17    // pub files: Vec<NuSpecFile>,
18}
19
20#[derive(Clone, Debug, Serialize, Deserialize)]
21struct NuSpecMetadata {
22    // Required fields
23    #[serde(rename = "id", default)]
24    pub id: String,
25    #[serde(rename = "version")]
26    pub version: String,
27    #[serde(rename = "description")]
28    pub description: String,
29    // TODO: comma-separated
30    #[serde(rename = "authors")]
31    pub authors: String,
32
33    // Attributes
34    #[serde(rename = "minClientVersion")]
35    pub min_client_version: Option<String>,
36
37    // Optional fields
38    // TODO: comma-separated
39    #[serde(rename = "owners")]
40    pub owners: Option<String>,
41    #[serde(rename = "projectUrl")]
42    pub project_url: Option<String>,
43    #[serde(rename = "licenseUrl")]
44    pub license_url: Option<String>,
45    #[serde(rename = "iconUrl")]
46    pub icon_url: Option<String>,
47    #[serde(rename = "icon")]
48    pub icon: Option<String>,
49    #[serde(rename = "readme")]
50    pub readme: Option<String>,
51    #[serde(rename = "requireLicenseAcceptance")]
52    pub require_license_acceptance: Option<bool>,
53    #[serde(rename = "license")]
54    pub license: Option<String>,
55    #[serde(rename = "copyright")]
56    pub copyright: Option<String>,
57    #[serde(rename = "developmentDependency")]
58    pub development_dependency: Option<bool>,
59    #[serde(rename = "releaseNotes")]
60    pub release_notes: Option<String>,
61    // TODO: space-separated
62    #[serde(rename = "tags")]
63    pub tags: Option<String>,
64    #[serde(rename = "language")]
65    pub language: Option<String>,
66    #[serde(rename = "repository")]
67    pub repository: Option<NuSpecRepository>,
68
69    // Collections
70    #[serde(rename = "dependencies")]
71    pub dependencies: Option<NuSpecDependencies>,
72    #[serde(rename = "frameworkAssemblies")]
73    pub framework_assemblies: Option<Vec<NuSpecFrameworkAssembly>>,
74    #[serde(rename = "packageTypes")]
75    pub package_types: Option<Vec<NuSpecPackageType>>,
76    #[serde(rename = "references")]
77    pub references: Option<Vec<NuSpecReference>>,
78    // #[serde(rename = "contentFiles")]
79    // pub content_files: Option<Vec<NuSpecContentFiles>>,
80}
81
82#[derive(Clone, Debug, Serialize, Deserialize)]
83struct NuSpecRepository {
84    #[serde(rename = "type")]
85    pub repo_type: Option<String>,
86    pub url: Option<String>,
87    pub branch: Option<String>,
88    pub commit: Option<String>,
89}
90
91#[derive(Clone, Debug, Serialize, Deserialize)]
92struct NuSpecFile {
93    pub src: String,
94    pub target: String,
95    pub exclude: String,
96}
97
98#[derive(Clone, Debug, Serialize, Deserialize)]
99struct NuSpecDependencies {
100    #[serde(rename = "group", default)]
101    groups: Vec<NuSpecDependencyGroup>,
102    #[serde(rename = "dependency", default)]
103    dependencies: Vec<NuSpecDependency>,
104}
105
106#[derive(Clone, Debug, Serialize, Deserialize)]
107#[serde(rename_all = "camelCase")]
108struct NuSpecDependencyGroup {
109    target_framework: Option<String>,
110    #[serde(rename = "dependency", default)]
111    dependencies: Vec<NuSpecDependency>,
112}
113
114#[derive(Clone, Debug, Serialize, Deserialize)]
115struct NuSpecDependency {
116    pub id: String,
117    pub version: String,
118    pub exclude: Option<String>,
119    pub include: Option<String>,
120}
121
122#[derive(Clone, Debug, Serialize, Deserialize)]
123#[serde(rename_all = "camelCase")]
124struct NuSpecFrameworkAssembly {
125    pub assembly_name: Option<String>,
126    pub target_framework: Option<String>,
127}
128
129#[derive(Clone, Debug, Serialize, Deserialize)]
130pub enum NuSpecPackageType {
131    Dependency,
132    DotnetTool,
133    Template,
134    #[serde(other)]
135    Unknown,
136}
137
138#[derive(Clone, Debug, Serialize, Deserialize)]
139enum NuSpecReferenceOrGroup {
140    Group {
141        #[serde(rename = "targetFramework")]
142        target_framework: String,
143        #[serde(rename = "reference", default)]
144        references: Vec<NuSpecReference>,
145    },
146    Reference(NuSpecReference),
147}
148
149#[derive(Clone, Debug, Serialize, Deserialize)]
150struct NuSpecReference {
151    pub file: String,
152}
153
154#[derive(Clone, Debug, Serialize, Deserialize)]
155struct NuSpecContentFiles {
156    pub include: String,
157    pub exclude: Option<String>,
158    #[serde(rename = "buildAction")]
159    pub build_action: Option<String>,
160    #[serde(rename = "copyToOutput")]
161    pub copy_to_output: Option<bool>,
162    pub flatten: Option<bool>,
163}
164
165pub struct DotnetNuSpec {}
166
167impl DotnetNuSpec {
168    pub fn new() -> Self {
169        Self {}
170    }
171
172    fn parse_dotnet_nuspec(path: impl AsRef<Path>) -> Result<NuSpec, SourcePkgError> {
173        let mut file = File::open(path)?;
174        let mut content = String::new();
175        file.read_to_string(&mut content)?;
176        let root = from_str::<NuSpec>(&content)?;
177        Ok(root)
178    }
179}
180
181#[async_trait::async_trait]
182impl PackageManifest for DotnetNuSpec {
183    fn get_name(&self) -> String {
184        "nuget".into()
185    }
186
187    async fn recognize(&self, path: &Path) -> Result<Package, SourcePkgError> {
188        let root = Self::parse_dotnet_nuspec(path)?;
189        let package = Package {
190            name: root.metadata.id,
191            version: root.metadata.version,
192            dependencies: root
193                .metadata
194                .dependencies
195                .into_iter()
196                .map(|dependencies| {
197                    let mut deps = vec![];
198                    for dependency_groups in dependencies.groups {
199                        let scope = dependency_groups.target_framework.unwrap_or_default();
200
201                        for dependency in dependency_groups.dependencies {
202                            let dep = DependentPackage {
203                                purl: PackageUrl::new("nuget", dependency.id)
204                                    .expect("purl arguments are invalid")
205                                    .to_string(),
206                                requirement: dependency.version,
207                                scope: scope.clone(),
208                                ..Default::default()
209                            };
210                            deps.push(dep);
211                        }
212                    }
213                    deps
214                })
215                .collect::<Vec<_>>()
216                .concat(),
217            ..Default::default()
218        };
219
220        Ok(package)
221    }
222
223    fn file_name_patterns(&self) -> &'static [&'static str] {
224        &["*.nuspec"]
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use crate::helper::testing::*;
232    use std::path::Path;
233
234    #[tokio::test]
235    async fn all_nuspec() {
236        let dir_path = Path::new(concat!(env!("CARGO_MANIFEST_DIR"), "/testdata/nuget"));
237        let parser = DotnetNuSpec::new();
238
239        for result in all_files_with_extensions(dir_path, &["nuspec"]) {
240            let path = result.expect("Failed to walk");
241            println!("{}", path.display());
242            let _ = parser.recognize(&path).await.expect("Failed to parse");
243        }
244    }
245}