Skip to main content

package_parser/pkgs/dotnet/
csproj.rs

1use packageurl::PackageUrl;
2
3use serde::Deserialize;
4use serde_xml_rs::Deserializer;
5
6use crate::error::SourcePkgError;
7use crate::pkgs::common::model::{DependentPackage, Package, PackageManifest};
8
9use std::path::Path;
10
11const INVALID_CHAR: &str = "\u{feff}";
12
13#[derive(Debug, Deserialize, PartialEq, Eq)]
14pub struct PackageReference {
15    #[serde(alias = "Include", default)]
16    pub name: String,
17
18    #[serde(alias = "Version", default)]
19    pub version: String,
20}
21
22#[derive(Debug, Deserialize, PartialEq)]
23struct ItemGroup {
24    #[serde(alias = "PackageReference", default)]
25    pub dependencies: Vec<PackageReference>,
26}
27
28#[derive(Debug, Deserialize, PartialEq)]
29struct Project {
30    #[serde(rename = "ItemGroup", default)]
31    pub item_groups: Vec<ItemGroup>,
32}
33
34#[derive(Debug, Clone, Deserialize)]
35#[serde(rename_all = "camelCase")]
36struct PackagesConfigItem {
37    id: String,
38    version: String,
39    target_framework: String,
40    development_dependency: Option<String>,
41}
42
43#[derive(Debug, Clone, Deserialize)]
44struct PackagesConfig {
45    package: Vec<PackagesConfigItem>,
46}
47
48fn parse(path: impl AsRef<Path>) -> Result<Package, SourcePkgError> {
49    let path = path.as_ref();
50
51    let mut deps = vec![];
52
53    // Check for `packages.config` file (XML).
54    let mut packages_config_path = path.to_owned();
55    packages_config_path.pop();
56    packages_config_path.push("packages.config");
57    if packages_config_path.exists() {
58        let content = std::fs::read_to_string(packages_config_path)?
59            .trim_start_matches(INVALID_CHAR)
60            .to_string();
61        let mut de =
62            Deserializer::new_from_reader(content.as_bytes()).non_contiguous_seq_elements(true);
63
64        match PackagesConfig::deserialize(&mut de) {
65            Ok(parsed) => {
66                for pkg in parsed.package {
67                    let dep = DependentPackage {
68                        purl: PackageUrl::new("nuget", pkg.id)
69                            .expect("purl arguments are invalid")
70                            .to_string(),
71                        requirement: pkg.version,
72                        scope: pkg.target_framework,
73                        is_resolved: true,
74                        is_runtime: if let Some(t) = pkg.development_dependency {
75                            t == "false"
76                        } else {
77                            true
78                        },
79                        ..Default::default()
80                    };
81                    deps.push(dep);
82                }
83            }
84            Err(e) => {
85                log::warn!("Failed to parse packages.config: {}", e);
86            }
87        }
88    }
89
90    let content = std::fs::read_to_string(path)?
91        .trim_start_matches(INVALID_CHAR)
92        .to_string();
93
94    let mut de =
95        Deserializer::new_from_reader(content.as_bytes()).non_contiguous_seq_elements(true);
96    let parsed =
97        Project::deserialize(&mut de).map_err(|e| SourcePkgError::GenericsError2(e.to_string()))?;
98
99    for item_group in parsed.item_groups {
100        deps.extend(item_group.dependencies.into_iter().map(|dependency| {
101            // version come from the csharp.csproj file must met the semver principle
102            // otherwise it can be dynamic upon the runtime
103            let locked_version = match semver::Version::parse(&dependency.version) {
104                Ok(version) => version.to_string(),
105                Err(_) => "".into(),
106            };
107
108            DependentPackage {
109                purl: PackageUrl::new("nuget", dependency.name)
110                    .expect("purl arguments are invalid")
111                    .to_string(),
112                requirement: locked_version.clone(),
113                is_resolved: !locked_version.is_empty(),
114                ..Default::default()
115            }
116        }));
117    }
118
119    let package = Package {
120        dependencies: deps,
121        ..Default::default()
122    };
123
124    Ok(package)
125}
126
127pub struct CSharpCsproj {}
128
129impl CSharpCsproj {
130    pub fn new() -> Self {
131        Self {}
132    }
133}
134
135#[async_trait::async_trait]
136impl PackageManifest for CSharpCsproj {
137    fn get_name(&self) -> String {
138        "nuget".into()
139    }
140
141    async fn recognize(&self, path: &Path) -> Result<Package, SourcePkgError> {
142        parse(path)
143    }
144
145    fn file_name_patterns(&self) -> &'static [&'static str] {
146        &["*.csproj"]
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn test_csproj() {
156        let filepath = Path::new(concat!(
157            env!("CARGO_MANIFEST_DIR"),
158            "/testdata/csharp/ICSharpCode.Decompiler.csproj"
159        ));
160
161        let p = parse(filepath).unwrap();
162        println!("{:?}", p);
163    }
164
165    #[test]
166    fn test_csproj_with_packages() {
167        let filepath = Path::new(concat!(
168            env!("CARGO_MANIFEST_DIR"),
169            "/testdata/csharp_with_package/Snyk.Common.csproj"
170        ));
171
172        let p = parse(filepath).unwrap();
173        println!("{:?}", p);
174    }
175}