Skip to main content

provenant/parsers/nuget/
nupkg.rs

1use std::fs::File;
2use std::io::Read;
3use std::path::Path;
4
5use crate::models::{DatasourceId, PackageData, PackageType};
6use crate::parser_warn as warn;
7
8use super::super::PackageParser;
9use super::default_package_data;
10use super::nuspec::parse_nuspec_content;
11
12const MAX_ARCHIVE_SIZE: u64 = 100 * 1024 * 1024;
13const MAX_FILE_SIZE: u64 = 50 * 1024 * 1024;
14const MAX_COMPRESSION_RATIO: f64 = 100.0;
15const MAX_UNCOMPRESSED_SIZE: u64 = 1024 * 1024 * 1024;
16
17pub struct NupkgParser;
18
19impl PackageParser for NupkgParser {
20    const PACKAGE_TYPE: PackageType = PackageType::Nuget;
21
22    fn is_match(path: &Path) -> bool {
23        path.extension()
24            .and_then(|ext| ext.to_str())
25            .is_some_and(|ext| ext == "nupkg")
26    }
27
28    fn extract_packages(path: &Path) -> Vec<PackageData> {
29        vec![match extract_nupkg_archive(path) {
30            Ok(data) => data,
31            Err(e) => {
32                warn!("Failed to extract .nupkg at {:?}: {}", path, e);
33                default_package_data(Some(DatasourceId::NugetNupkg))
34            }
35        }]
36    }
37}
38
39fn extract_nupkg_archive(path: &Path) -> Result<PackageData, String> {
40    use std::fs;
41    use zip::ZipArchive;
42
43    let file_metadata =
44        fs::metadata(path).map_err(|e| format!("Failed to read file metadata: {}", e))?;
45    let archive_size = file_metadata.len();
46
47    if archive_size > MAX_ARCHIVE_SIZE {
48        return Err(format!(
49            "Archive too large: {} bytes (limit: {} bytes)",
50            archive_size, MAX_ARCHIVE_SIZE
51        ));
52    }
53
54    let file = File::open(path).map_err(|e| format!("Failed to open archive: {}", e))?;
55    let mut archive =
56        ZipArchive::new(file).map_err(|e| format!("Failed to read ZIP archive: {}", e))?;
57
58    let mut total_uncompressed: u64 = 0;
59
60    for i in 0..archive.len() {
61        let content = {
62            let mut entry = archive
63                .by_index(i)
64                .map_err(|e| format!("Failed to read ZIP entry: {}", e))?;
65
66            let entry_name = entry.name().to_string();
67            let entry_size = entry.size();
68
69            total_uncompressed += entry_size;
70            if total_uncompressed > MAX_UNCOMPRESSED_SIZE {
71                warn!(
72                    "NuGet: total uncompressed size exceeds {} bytes for {:?}",
73                    MAX_UNCOMPRESSED_SIZE, path
74                );
75                return Err(format!(
76                    "Total uncompressed size exceeds limit: {} bytes (limit: {} bytes)",
77                    total_uncompressed, MAX_UNCOMPRESSED_SIZE
78                ));
79            }
80
81            if !entry_name.ends_with(".nuspec") {
82                continue;
83            }
84
85            if entry_size > MAX_FILE_SIZE {
86                return Err(format!(
87                    ".nuspec too large: {} bytes (limit: {} bytes)",
88                    entry_size, MAX_FILE_SIZE
89                ));
90            }
91
92            let compressed_size = entry.compressed_size();
93            if compressed_size > 0 {
94                let ratio = entry_size as f64 / compressed_size as f64;
95                if ratio > MAX_COMPRESSION_RATIO {
96                    return Err(format!(
97                        "Suspicious compression ratio: {:.2}:1 (limit: {:.0}:1)",
98                        ratio, MAX_COMPRESSION_RATIO
99                    ));
100                }
101            }
102
103            let mut content = String::new();
104            entry
105                .read_to_string(&mut content)
106                .map_err(|e| format!("Failed to read .nuspec: {}", e))?;
107            content
108        };
109
110        let mut package_data = parse_nuspec_content(&content)?;
111
112        let license_file = package_data.extra_data.as_ref().and_then(|extra| {
113            extra
114                .get("license_file")
115                .and_then(|value| value.as_str())
116                .map(|value| value.to_string())
117        });
118
119        if let Some(license_file) = license_file
120            && let Some(license_text) = read_nupkg_license_file(&mut archive, &license_file)?
121        {
122            package_data.extracted_license_statement = Some(license_text);
123        }
124
125        return Ok(package_data);
126    }
127
128    Err("No .nuspec file found in archive".to_string())
129}
130
131fn read_nupkg_license_file(
132    archive: &mut zip::ZipArchive<File>,
133    license_file: &str,
134) -> Result<Option<String>, String> {
135    if license_file.split('/').any(|c| c == "..") || license_file.split('\\').any(|c| c == "..") {
136        warn!(
137            "NuGet: path traversal detected in license file path: {}",
138            license_file
139        );
140        return Ok(None);
141    }
142
143    let normalized_target = license_file.replace('\\', "/");
144
145    for i in 0..archive.len() {
146        let mut entry = archive
147            .by_index(i)
148            .map_err(|e| format!("Failed to read ZIP entry: {}", e))?;
149        let entry_name = entry.name().replace('\\', "/");
150
151        if entry_name != normalized_target
152            && !entry_name.ends_with(&format!("/{}", normalized_target))
153        {
154            continue;
155        }
156
157        let entry_size = entry.size();
158        if entry_size > MAX_FILE_SIZE {
159            return Err(format!(
160                "License file too large: {} bytes (limit: {} bytes)",
161                entry_size, MAX_FILE_SIZE
162            ));
163        }
164
165        let mut content = Vec::new();
166        entry
167            .read_to_end(&mut content)
168            .map_err(|e| format!("Failed to read license file from archive: {}", e))?;
169
170        return Ok(Some(String::from_utf8_lossy(&content).to_string()));
171    }
172
173    Ok(None)
174}
175
176crate::register_parser!(
177    ".NET .nupkg package archive",
178    &["**/*.nupkg"],
179    "nuget",
180    "C#",
181    Some("https://learn.microsoft.com/en-us/nuget/create-packages/creating-a-package"),
182);