Skip to main content

provenant/parsers/
misc.rs

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