Skip to main content

provenant/parsers/
misc.rs

1// SPDX-FileCopyrightText: nexB Inc. and others
2// SPDX-FileCopyrightText: Provenant contributors
3// SPDX-License-Identifier: Apache-2.0
4// Derived from ScanCode Toolkit (Apache-2.0); modified. See NOTICE.
5
6//! File type recognizers for various package archives and binary formats.
7//!
8//! This module contains simple file-type recognizers that identify packages by
9//! their file extensions or path patterns. These recognizers do NOT parse file
10//! contents - they only tag files with the appropriate package_type and datasource_id.
11//!
12//! # Implementation Notes
13//!
14//! - Most recognizers use the `file_recognizer!` macro to reduce boilerplate and
15//!   return minimal PackageData with only package_type and datasource_id set
16//! - These correspond to Python's misc.py NonAssemblableDatafileHandler classes
17//! - The JVM-archive recognizers (`.jar`/`.war`/`.aar`) are an exception: they
18//!   perform bounded ZIP introspection of `META-INF/MANIFEST.MF` and
19//!   `META-INF/maven/**/pom.properties` to recover real package metadata
20//! - Some recognizers use magic byte detection for disambiguation (Squashfs, NSIS, InstallShield)
21
22use std::path::Path;
23
24use super::PackageParser;
25use super::maven::{JvmArchiveKind, extract_jvm_archive};
26use crate::models::{DatasourceId, PackageData, PackageType};
27use crate::utils::magic;
28
29/// Helper macro to define file-type recognizers with minimal boilerplate.
30///
31/// Each recognizer matches specific file patterns and returns a minimal
32/// PackageData structure with only package_type and datasource_id populated.
33///
34/// # Arguments
35///
36/// * `$name` - Struct name for the recognizer
37/// * `$pkg_type` - Package type string (e.g., "jar", "war", "meteor")
38/// * `$datasource` - Datasource ID string (e.g., "java_jar", "meteor_package")
39/// * `$match_fn` - Closure that takes a &Path and returns bool for matching
40macro_rules! file_recognizer {
41    ($name:ident, $pkg_type:expr, $datasource:expr, $match_fn:expr) => {
42        pub struct $name;
43
44        impl PackageParser for $name {
45            const PACKAGE_TYPE: PackageType = $pkg_type;
46
47            fn is_match(path: &Path) -> bool {
48                ($match_fn)(path)
49            }
50
51            fn extract_packages(path: &Path) -> Vec<PackageData> {
52                let _ = path;
53                vec![PackageData {
54                    package_type: Some($pkg_type),
55                    datasource_id: Some($datasource),
56                    ..Default::default()
57                }]
58            }
59        }
60    };
61}
62
63// Java Archives
64
65/// `.jar` archive introspector.
66///
67/// Reads `META-INF/MANIFEST.MF` and `META-INF/maven/**/pom.properties` from the
68/// archive via the shared bounded-ZIP reader and falls back to the bare
69/// recognizer row when no metadata can be recovered.
70pub struct JavaJarRecognizer;
71
72impl PackageParser for JavaJarRecognizer {
73    const PACKAGE_TYPE: PackageType = PackageType::Jar;
74
75    fn is_match(path: &Path) -> bool {
76        path.extension().and_then(|e| e.to_str()) == Some("jar")
77    }
78
79    fn extract_packages(path: &Path) -> Vec<PackageData> {
80        extract_jvm_archive(path, JvmArchiveKind::Jar)
81    }
82}
83
84/// `.war` web-application archive introspector. See [`JavaJarRecognizer`].
85pub struct JavaWarRecognizer;
86
87impl PackageParser for JavaWarRecognizer {
88    const PACKAGE_TYPE: PackageType = PackageType::War;
89
90    fn is_match(path: &Path) -> bool {
91        path.extension().and_then(|e| e.to_str()) == Some("war")
92    }
93
94    fn extract_packages(path: &Path) -> Vec<PackageData> {
95        extract_jvm_archive(path, JvmArchiveKind::War)
96    }
97}
98
99file_recognizer!(
100    JavaWarWebXmlRecognizer,
101    PackageType::War,
102    DatasourceId::JavaWarWebXml,
103    |path: &Path| path
104        .to_str()
105        .is_some_and(|p| p.ends_with("/WEB-INF/web.xml") || p.ends_with("WEB-INF/web.xml"))
106);
107
108file_recognizer!(
109    JavaEarRecognizer,
110    PackageType::Ear,
111    DatasourceId::JavaEarArchive,
112    |path: &Path| path.extension().and_then(|e| e.to_str()) == Some("ear")
113);
114
115file_recognizer!(
116    JavaEarAppXmlRecognizer,
117    PackageType::Ear,
118    DatasourceId::JavaEarApplicationXml,
119    |path: &Path| path.to_str().is_some_and(
120        |p| p.ends_with("/META-INF/application.xml") || p.ends_with("META-INF/application.xml")
121    )
122);
123
124// Apache Axis2
125
126file_recognizer!(
127    Axis2ModuleXmlRecognizer,
128    PackageType::Axis2,
129    DatasourceId::Axis2ModuleXml,
130    |path: &Path| {
131        path.to_str().is_some_and(|p| {
132            let lower = p.to_lowercase();
133            lower.ends_with("/meta-inf/module.xml") || lower.ends_with("meta-inf/module.xml")
134        })
135    }
136);
137
138file_recognizer!(
139    Axis2MarRecognizer,
140    PackageType::Axis2,
141    DatasourceId::Axis2Mar,
142    |path: &Path| path.extension().and_then(|e| e.to_str()) == Some("mar")
143);
144
145// JBoss
146
147file_recognizer!(
148    JBossSarRecognizer,
149    PackageType::JbossService,
150    DatasourceId::JbossSar,
151    |path: &Path| path.extension().and_then(|e| e.to_str()) == Some("sar")
152);
153
154file_recognizer!(
155    JBossServiceXmlRecognizer,
156    PackageType::JbossService,
157    DatasourceId::JbossServiceXml,
158    |path: &Path| {
159        path.to_str().is_some_and(|p| {
160            let lower = p.to_lowercase();
161            lower.ends_with("/meta-inf/jboss-service.xml")
162                || lower.ends_with("meta-inf/jboss-service.xml")
163        })
164    }
165);
166
167// Meteor
168
169file_recognizer!(
170    MeteorPackageRecognizer,
171    PackageType::Meteor,
172    DatasourceId::MeteorPackage,
173    |path: &Path| path.to_str().is_some_and(|p| p.ends_with("/package.js"))
174);
175
176// Mobile Apps
177
178/// `.aar` Android library archive introspector. See [`JavaJarRecognizer`].
179pub struct AndroidLibraryRecognizer;
180
181impl PackageParser for AndroidLibraryRecognizer {
182    const PACKAGE_TYPE: PackageType = PackageType::AndroidLib;
183
184    fn is_match(path: &Path) -> bool {
185        path.extension().and_then(|e| e.to_str()) == Some("aar")
186    }
187
188    fn extract_packages(path: &Path) -> Vec<PackageData> {
189        extract_jvm_archive(path, JvmArchiveKind::Aar)
190    }
191}
192
193file_recognizer!(
194    MozillaXpiRecognizer,
195    PackageType::Mozilla,
196    DatasourceId::MozillaXpi,
197    |path: &Path| path.extension().and_then(|e| e.to_str()) == Some("xpi")
198);
199
200file_recognizer!(
201    ChromeCrxRecognizer,
202    PackageType::Chrome,
203    DatasourceId::ChromeCrx,
204    |path: &Path| path.extension().and_then(|e| e.to_str()) == Some("crx")
205);
206
207file_recognizer!(
208    IosIpaRecognizer,
209    PackageType::Ios,
210    DatasourceId::IosIpa,
211    |path: &Path| path.extension().and_then(|e| e.to_str()) == Some("ipa")
212);
213
214// Archives
215
216file_recognizer!(
217    CabArchiveRecognizer,
218    PackageType::Cab,
219    DatasourceId::MicrosoftCabinet,
220    |path: &Path| path.extension().and_then(|e| e.to_str()) == Some("cab")
221);
222
223file_recognizer!(
224    SharArchiveRecognizer,
225    PackageType::Shar,
226    DatasourceId::SharShellArchive,
227    |path: &Path| path.extension().and_then(|e| e.to_str()) == Some("shar")
228);
229
230// Disk Images
231
232file_recognizer!(
233    AppleDmgRecognizer,
234    PackageType::Dmg,
235    DatasourceId::AppleDmg,
236    |path: &Path| {
237        path.extension()
238            .and_then(|e| e.to_str())
239            .is_some_and(|ext| ext == "dmg" || ext == "sparseimage")
240    }
241);
242
243file_recognizer!(
244    IsoImageRecognizer,
245    PackageType::Iso,
246    DatasourceId::IsoDiskImage,
247    |path: &Path| {
248        path.extension()
249            .and_then(|e| e.to_str())
250            .is_some_and(|ext| ext == "iso" || ext == "udf" || ext == "img")
251    }
252);
253
254// Installers and Binary Formats (require magic byte detection)
255
256file_recognizer!(
257    SquashfsRecognizer,
258    PackageType::Squashfs,
259    DatasourceId::SquashfsDiskImage,
260    |path: &Path| magic::is_squashfs(path)
261);
262
263file_recognizer!(
264    NsisRecognizer,
265    PackageType::Nsis,
266    DatasourceId::NsisInstaller,
267    |path: &Path| {
268        path.extension()
269            .and_then(|e| e.to_str())
270            .is_some_and(|ext| ext == "exe")
271            && magic::is_nsis_installer(path)
272    }
273);
274
275file_recognizer!(
276    InstallShieldRecognizer,
277    PackageType::Installshield,
278    DatasourceId::InstallshieldInstaller,
279    |path: &Path| {
280        path.extension()
281            .and_then(|e| e.to_str())
282            .is_some_and(|ext| ext == "exe")
283            && magic::is_zip(path)
284    }
285);
286
287pub(crate) static RECOGNIZER_METADATA: &[super::metadata::ParserMetadata] = &[
288    super::metadata::ParserMetadata {
289        description: "Misc file type recognizers (JAR, WAR, EAR, AAR, iOS, Chrome, Mozilla, installers, disk images, etc.)",
290        file_patterns: &[
291            "**/*.jar",
292            "**/*.war",
293            "**/WEB-INF/web.xml",
294            "**/*.ear",
295            "**/META-INF/application.xml",
296            "**/meta-inf/module.xml",
297            "**/*.mar",
298            "**/*.sar",
299            "**/meta-inf/jboss-service.xml",
300            "**/package.js",
301            "**/*.aar",
302            "**/*.xpi",
303            "**/*.crx",
304            "**/*.ipa",
305            "**/*.cab",
306            "**/*.shar",
307            "**/*.dmg",
308            "**/*.sparseimage",
309            "**/*.iso",
310            "**/*.udf",
311            "**/*.img",
312            "**/*.exe",
313        ],
314        package_type: "",
315        primary_language: "",
316        documentation_url: None,
317    },
318];