1use std::path::Path;
26
27use crate::parser_warn as warn;
28
29use crate::models::{DatasourceId, PackageData, PackageType};
30use crate::models::{Dependency, FileReference};
31use crate::parsers::utils::{MAX_MANIFEST_SIZE, capped_iteration_limit, truncate_field};
32
33use super::PackageParser;
34use super::license_normalization::{
35 DeclaredLicenseMatchMetadata, build_declared_license_data, empty_declared_license_data,
36};
37use super::rpm_db_native::{InstalledRpmDbKind, InstalledRpmPackage, read_installed_rpm_packages};
38use super::rpm_parser::infer_rpm_namespace;
39use super::rpm_parser::infer_rpm_namespace_from_filename;
40use super::rpm_parser::normalize_rpm_declared_license;
41
42const PACKAGE_TYPE: PackageType = PackageType::Rpm;
43const RPM_BDB_PATH_SUFFIXES: &[&str] = &["var/lib/rpm/Packages", "usr/lib/sysimage/rpm/Packages"];
44const RPM_NDB_PATH_SUFFIXES: &[&str] = &[
45 "var/lib/rpm/Packages.db",
46 "usr/lib/sysimage/rpm/Packages.db",
47];
48#[cfg(feature = "rpm-sqlite")]
49const RPM_SQLITE_PATH_SUFFIXES: &[&str] = &[
50 "var/lib/rpm/rpmdb.sqlite",
51 "usr/lib/sysimage/rpm/rpmdb.sqlite",
52];
53
54#[derive(Debug)]
55struct RpmQueryPackage {
56 name: Option<String>,
57 epoch: Option<String>,
58 version: Option<String>,
59 release: Option<String>,
60 vendor: Option<String>,
61 distribution: Option<String>,
62 arch: Option<String>,
63 platform: Option<String>,
64 size: Option<u64>,
65 license: Option<String>,
66 source_rpm: Option<String>,
67 requires: Vec<String>,
68 file_names: Vec<Option<String>>,
69 dir_indexes: Vec<u32>,
70 base_names: Vec<Option<String>>,
71 dir_names: Vec<String>,
72}
73
74fn default_package_data(datasource_id: DatasourceId) -> PackageData {
75 PackageData {
76 package_type: Some(PACKAGE_TYPE),
77 datasource_id: Some(datasource_id),
78 ..Default::default()
79 }
80}
81
82pub struct RpmBdbDatabaseParser;
83
84#[cfg(feature = "rpm-sqlite")]
88impl PackageParser for RpmBdbDatabaseParser {
89 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
90
91 fn is_match(path: &Path) -> bool {
92 path_matches_any_suffix(path, RPM_BDB_PATH_SUFFIXES)
93 }
94
95 fn extract_packages(path: &Path) -> Vec<PackageData> {
96 match parse_rpm_database(path, DatasourceId::RpmInstalledDatabaseBdb) {
97 Ok(pkgs) if !pkgs.is_empty() => pkgs,
98 Ok(_) => vec![default_package_data(DatasourceId::RpmInstalledDatabaseBdb)],
99 Err(e) => {
100 warn!("Failed to parse RPM BDB database {:?}: {}", path, e);
101 vec![default_package_data(DatasourceId::RpmInstalledDatabaseBdb)]
102 }
103 }
104 }
105
106 fn metadata() -> Vec<super::metadata::ParserMetadata> {
107 vec![super::metadata::ParserMetadata {
108 description: "RPM installed package database",
109 file_patterns: &[
110 "**/var/lib/rpm/Packages",
111 "**/usr/lib/sysimage/rpm/Packages",
112 "**/var/lib/rpm/Packages.db",
113 "**/usr/lib/sysimage/rpm/Packages.db",
114 "**/var/lib/rpm/rpmdb.sqlite",
115 "**/usr/lib/sysimage/rpm/rpmdb.sqlite",
116 ],
117 package_type: "rpm",
118 primary_language: "",
119 documentation_url: Some("https://rpm.org/"),
120 }]
121 }
122}
123
124#[cfg(not(feature = "rpm-sqlite"))]
125impl PackageParser for RpmBdbDatabaseParser {
126 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
127
128 fn is_match(path: &Path) -> bool {
129 path_matches_any_suffix(path, RPM_BDB_PATH_SUFFIXES)
130 }
131
132 fn extract_packages(path: &Path) -> Vec<PackageData> {
133 match parse_rpm_database(path, DatasourceId::RpmInstalledDatabaseBdb) {
134 Ok(pkgs) if !pkgs.is_empty() => pkgs,
135 Ok(_) => vec![default_package_data(DatasourceId::RpmInstalledDatabaseBdb)],
136 Err(e) => {
137 warn!("Failed to parse RPM BDB database {:?}: {}", path, e);
138 vec![default_package_data(DatasourceId::RpmInstalledDatabaseBdb)]
139 }
140 }
141 }
142
143 fn metadata() -> Vec<super::metadata::ParserMetadata> {
144 vec![super::metadata::ParserMetadata {
145 description: "RPM installed package database",
146 file_patterns: &[
147 "**/var/lib/rpm/Packages",
148 "**/usr/lib/sysimage/rpm/Packages",
149 "**/var/lib/rpm/Packages.db",
150 "**/usr/lib/sysimage/rpm/Packages.db",
151 ],
152 package_type: "rpm",
153 primary_language: "",
154 documentation_url: Some("https://rpm.org/"),
155 }]
156 }
157}
158
159pub struct RpmNdbDatabaseParser;
160
161impl PackageParser for RpmNdbDatabaseParser {
162 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
163
164 fn is_match(path: &Path) -> bool {
165 path_matches_any_suffix(path, RPM_NDB_PATH_SUFFIXES)
166 }
167
168 fn extract_packages(path: &Path) -> Vec<PackageData> {
169 match parse_rpm_database(path, DatasourceId::RpmInstalledDatabaseNdb) {
170 Ok(pkgs) if !pkgs.is_empty() => pkgs,
171 Ok(_) => vec![default_package_data(DatasourceId::RpmInstalledDatabaseNdb)],
172 Err(e) => {
173 warn!("Failed to parse RPM NDB database {:?}: {}", path, e);
174 vec![default_package_data(DatasourceId::RpmInstalledDatabaseNdb)]
175 }
176 }
177 }
178}
179
180#[cfg(feature = "rpm-sqlite")]
181pub struct RpmSqliteDatabaseParser;
182
183#[cfg(feature = "rpm-sqlite")]
184impl PackageParser for RpmSqliteDatabaseParser {
185 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
186
187 fn is_match(path: &Path) -> bool {
188 path_matches_any_suffix(path, RPM_SQLITE_PATH_SUFFIXES)
189 }
190
191 fn extract_packages(path: &Path) -> Vec<PackageData> {
192 match parse_rpm_database(path, DatasourceId::RpmInstalledDatabaseSqlite) {
193 Ok(pkgs) if !pkgs.is_empty() => pkgs,
194 Ok(_) => vec![default_package_data(
195 DatasourceId::RpmInstalledDatabaseSqlite,
196 )],
197 Err(e) => {
198 warn!("Failed to parse RPM SQLite database {:?}: {}", path, e);
199 vec![default_package_data(
200 DatasourceId::RpmInstalledDatabaseSqlite,
201 )]
202 }
203 }
204 }
205}
206
207fn parse_rpm_database(
208 path: &Path,
209 datasource_id: DatasourceId,
210) -> Result<Vec<PackageData>, String> {
211 let metadata = std::fs::metadata(path)
212 .map_err(|e| format!("Cannot stat RPM database file {:?}: {}", path, e))?;
213
214 if metadata.len() > MAX_MANIFEST_SIZE {
215 return Err(format!(
216 "RPM database file {:?} is {} bytes, exceeding the {} byte limit",
217 path,
218 metadata.len(),
219 MAX_MANIFEST_SIZE
220 ));
221 }
222
223 let native_kind = native_kind_for_datasource(datasource_id)?;
224 match read_installed_rpm_packages(path, native_kind) {
225 Ok(packages) => {
226 let limit = capped_iteration_limit(packages.len(), "rpm_db native packages");
227 Ok(packages
228 .into_iter()
229 .take(limit)
230 .map(native_package_to_query_package)
231 .map(|pkg| build_package_data(pkg, datasource_id))
232 .collect())
233 }
234 Err(native_error) => Err(format!(
235 "native installed RPM reader failed for {:?}: {}",
236 path, native_error
237 )),
238 }
239}
240
241fn path_matches_suffix(path: &Path, suffix: &str) -> bool {
242 path.to_string_lossy().replace('\\', "/").ends_with(suffix)
243}
244
245fn path_matches_any_suffix(path: &Path, suffixes: &[&str]) -> bool {
246 suffixes
247 .iter()
248 .any(|suffix| path_matches_suffix(path, suffix))
249}
250
251fn native_kind_for_datasource(datasource_id: DatasourceId) -> Result<InstalledRpmDbKind, String> {
252 match datasource_id {
253 DatasourceId::RpmInstalledDatabaseBdb => Ok(InstalledRpmDbKind::Bdb),
254 DatasourceId::RpmInstalledDatabaseNdb => Ok(InstalledRpmDbKind::Ndb),
255 DatasourceId::RpmInstalledDatabaseSqlite => Ok(InstalledRpmDbKind::Sqlite),
256 other => Err(format!(
257 "unexpected datasource for installed RPM DB: {other:?}"
258 )),
259 }
260}
261
262fn native_package_to_query_package(package: InstalledRpmPackage) -> RpmQueryPackage {
263 let requires_limit = capped_iteration_limit(package.requires.len(), "rpm_db package requires");
264 let file_names_limit =
265 capped_iteration_limit(package.file_names.len(), "rpm_db package file_names");
266 let base_names_limit =
267 capped_iteration_limit(package.base_names.len(), "rpm_db package base_names");
268 let dir_names_limit =
269 capped_iteration_limit(package.dir_names.len(), "rpm_db package dir_names");
270 RpmQueryPackage {
271 name: truncate_optional_string(Some(package.name)),
272 epoch: Some(package.epoch.to_string()),
273 version: truncate_optional_string(Some(package.version)),
274 release: truncate_optional_string(Some(package.release)),
275 vendor: truncate_optional_string(Some(package.vendor)),
276 distribution: truncate_optional_string(Some(package.distribution)),
277 arch: truncate_optional_string(Some(package.arch)),
278 platform: truncate_optional_string(Some(package.platform)),
279 size: (package.size > 0).then_some(u64::from(package.size)),
280 license: truncate_optional_string(Some(package.license)),
281 source_rpm: truncate_optional_string(Some(package.source_rpm)),
282 requires: package
283 .requires
284 .into_iter()
285 .take(requires_limit)
286 .map(truncate_field)
287 .collect(),
288 file_names: package
289 .file_names
290 .into_iter()
291 .take(file_names_limit)
292 .map(|s| Some(truncate_field(s)))
293 .collect(),
294 dir_indexes: package.dir_indexes,
295 base_names: package
296 .base_names
297 .into_iter()
298 .take(base_names_limit)
299 .map(|s| Some(truncate_field(s)))
300 .collect(),
301 dir_names: package
302 .dir_names
303 .into_iter()
304 .take(dir_names_limit)
305 .map(truncate_field)
306 .collect(),
307 }
308}
309
310fn truncate_optional_string(value: Option<String>) -> Option<String> {
311 value
312 .map(truncate_field)
313 .and_then(|v| normalize_optional_string(Some(v)))
314}
315
316fn build_version_release(version: &str, release: &str) -> Option<String> {
317 if version.is_empty() {
318 return None;
319 }
320
321 let mut vr = String::from(version);
322
323 if !release.is_empty() {
324 vr.push('-');
325 vr.push_str(release);
326 }
327
328 Some(vr)
329}
330
331fn build_file_references(
332 base_names: &[Option<String>],
333 dir_indexes: &[u32],
334 dir_names: &[String],
335) -> Vec<FileReference> {
336 if base_names.is_empty() || dir_names.is_empty() {
337 return Vec::new();
338 }
339
340 let limit = capped_iteration_limit(
341 base_names.len().min(dir_indexes.len()),
342 "rpm_db file references",
343 );
344 base_names
345 .iter()
346 .zip(dir_indexes.iter())
347 .take(limit)
348 .filter_map(|(basename, &dir_idx)| {
349 let dirname = dir_names.get(dir_idx as usize)?;
350 let basename = basename.as_deref().unwrap_or_default();
351 let path = format!("{}{}", dirname, basename);
352 if path.is_empty() || path == "/" {
353 return None;
354 }
355 Some(FileReference {
356 path,
357 size: None,
358 sha1: None,
359 md5: None,
360 sha256: None,
361 sha512: None,
362 extra_data: None,
363 })
364 })
365 .collect()
366}
367
368fn build_file_references_from_paths(paths: &[Option<String>]) -> Vec<FileReference> {
369 let limit = capped_iteration_limit(paths.len(), "rpm_db file references from paths");
370 paths
371 .iter()
372 .take(limit)
373 .filter_map(|path| {
374 let path = path.as_deref()?.trim();
375 if path.is_empty() || path == "/" {
376 return None;
377 }
378
379 Some(FileReference {
380 path: path.to_string(),
381 size: None,
382 sha1: None,
383 md5: None,
384 sha256: None,
385 sha512: None,
386 extra_data: None,
387 })
388 })
389 .collect()
390}
391
392fn build_package_data(pkg: RpmQueryPackage, datasource_id: DatasourceId) -> PackageData {
393 let name = normalize_optional_string(pkg.name).map(truncate_field);
394 let version_raw = normalize_optional_string(pkg.version).map(truncate_field);
395 let release = normalize_optional_string(pkg.release).map(truncate_field);
396 let version = build_version_release(
397 version_raw.as_deref().unwrap_or_default(),
398 release.as_deref().unwrap_or_default(),
399 );
400 let epoch_value = parse_epoch(pkg.epoch);
403 let epoch = (epoch_value > 0).then(|| epoch_value.to_string());
404
405 let vendor = normalize_optional_string(pkg.vendor)
406 .map(truncate_field)
407 .or_else(|| normalize_optional_string(pkg.distribution).map(truncate_field));
408 let source_rpm = normalize_optional_string(pkg.source_rpm).map(truncate_field);
409 let namespace =
410 infer_rpm_namespace(None, vendor.as_deref(), release.as_deref(), None).or_else(|| {
411 source_rpm
412 .as_deref()
413 .and_then(|source_rpm| infer_rpm_namespace_from_filename(Path::new(source_rpm)))
414 });
415
416 let architecture = normalize_optional_string(pkg.arch)
417 .map(truncate_field)
418 .or_else(|| infer_platform_architecture(pkg.platform.as_deref()));
419 let requires_limit = capped_iteration_limit(pkg.requires.len(), "rpm_db package dependencies");
420 let dependencies = pkg
421 .requires
422 .into_iter()
423 .take(requires_limit)
424 .filter_map(|require| build_dependency(&require))
425 .collect();
426 let extracted_license_statement = normalize_optional_string(pkg.license).map(truncate_field);
427 let (declared_license_expression, declared_license_expression_spdx, license_detections) =
428 extracted_license_statement
429 .as_deref()
430 .and_then(normalize_rpm_declared_license)
431 .map(|normalized| {
432 build_declared_license_data(
433 normalized,
434 DeclaredLicenseMatchMetadata::single_line(
435 extracted_license_statement.as_deref().unwrap_or_default(),
436 ),
437 )
438 })
439 .map(|(expr, spdx, detections)| {
440 (
441 expr.map(truncate_field),
442 spdx.map(truncate_field),
443 detections,
444 )
445 })
446 .unwrap_or_else(empty_declared_license_data);
447 let source_packages = source_rpm
448 .as_deref()
449 .and_then(source_rpm_purl)
450 .into_iter()
451 .collect();
452 let file_references = {
453 let from_dir_components =
454 build_file_references(&pkg.base_names, &pkg.dir_indexes, &pkg.dir_names);
455 if from_dir_components.is_empty() {
456 build_file_references_from_paths(&pkg.file_names)
457 } else {
458 from_dir_components
459 }
460 };
461 let purl = build_package_purl(
462 name.as_deref(),
463 namespace.as_deref(),
464 version.as_deref(),
465 architecture.as_deref(),
466 epoch.as_deref(),
467 );
468
469 PackageData {
470 datasource_id: Some(datasource_id),
471 package_type: Some(PACKAGE_TYPE),
472 namespace,
473 name,
474 version,
475 qualifiers: {
476 let mut q = std::collections::HashMap::new();
477 if let Some(arch) = &architecture {
478 q.insert("arch".to_string(), arch.clone());
479 }
480 if let Some(epoch) = &epoch {
481 q.insert("epoch".to_string(), epoch.clone());
482 }
483 (!q.is_empty()).then_some(q)
484 },
485 subpath: None,
486 primary_language: None,
487 description: None,
488 release_date: None,
489 parties: Vec::new(),
490 keywords: Vec::new(),
491 homepage_url: None,
492 download_url: None,
493 size: pkg.size.filter(|size| *size > 0),
494 sha1: None,
495 md5: None,
496 sha256: None,
497 sha512: None,
498 bug_tracking_url: None,
499 code_view_url: None,
500 vcs_url: None,
501 copyright: None,
502 holder: None,
503 declared_license_expression,
504 declared_license_expression_spdx,
505 license_detections,
506 other_license_expression: None,
507 other_license_expression_spdx: None,
508 other_license_detections: Vec::new(),
509 extracted_license_statement,
510 notice_text: None,
511 source_packages,
512 file_references,
513 is_private: false,
514 is_virtual: false,
515 extra_data: None,
516 dependencies,
517 repository_homepage_url: None,
518 repository_download_url: None,
519 api_data_url: None,
520 purl,
521 }
522}
523
524fn build_dependency(require: &str) -> Option<Dependency> {
525 let require = require.trim();
526 if require.is_empty() || require.starts_with("rpmlib(") || require.starts_with("config(") {
527 return None;
528 }
529
530 let purl = packageurl::PackageUrl::new(PACKAGE_TYPE.as_str(), require)
531 .ok()
532 .map(|p| p.to_string());
533
534 Some(Dependency {
535 purl,
536 extracted_requirement: None,
537 scope: Some("requires".to_string()),
538 is_runtime: Some(true),
539 is_optional: Some(false),
540 is_pinned: Some(false),
541 is_direct: Some(true),
542 resolved_package: None,
543 extra_data: None,
544 })
545}
546
547pub(super) fn source_rpm_purl(source_rpm: &str) -> Option<String> {
558 let stem = source_rpm.strip_suffix(".rpm")?;
559 let (name_version_release, arch) = stem.rsplit_once('.')?;
560 let (name_version, release) = name_version_release.rsplit_once('-')?;
561 let (name, version) = name_version.rsplit_once('-')?;
562 if name.is_empty() || version.is_empty() || release.is_empty() {
563 return None;
564 }
565
566 build_package_purl(
567 Some(name),
568 None,
569 Some(&format!("{version}-{release}")),
570 Some(arch),
571 None,
572 )
573}
574
575fn build_package_purl(
576 name: Option<&str>,
577 namespace: Option<&str>,
578 version: Option<&str>,
579 arch: Option<&str>,
580 epoch: Option<&str>,
581) -> Option<String> {
582 let name = name?;
583 let mut purl = packageurl::PackageUrl::new(PACKAGE_TYPE.as_str(), name).ok()?;
584
585 if let Some(namespace) = namespace {
586 purl.with_namespace(namespace).ok()?;
587 }
588
589 if let Some(version) = version {
590 purl.with_version(version).ok()?;
591 }
592
593 if let Some(arch) = arch {
594 purl.add_qualifier("arch", arch).ok()?;
595 }
596
597 if let Some(epoch) = epoch {
599 purl.add_qualifier("epoch", epoch).ok()?;
600 }
601
602 Some(purl.to_string())
603}
604
605fn normalize_optional_string(value: Option<String>) -> Option<String> {
606 value.and_then(|value| {
607 let trimmed = value.trim();
608 if trimmed.is_empty() || trimmed == "(none)" || trimmed == "[]" {
609 None
610 } else {
611 Some(trimmed.to_string())
612 }
613 })
614}
615
616fn parse_epoch(value: Option<String>) -> u32 {
617 normalize_optional_string(value)
618 .and_then(|value| value.parse::<u32>().ok())
619 .unwrap_or(0)
620}
621
622fn infer_platform_architecture(platform: Option<&str>) -> Option<String> {
623 let platform = platform?.trim();
624 if platform.is_empty() {
625 return None;
626 }
627
628 platform
629 .split_once('-')
630 .map(|(arch, _)| arch)
631 .filter(|arch| !arch.is_empty())
632 .map(|arch| arch.to_string())
633}
634
635#[cfg(test)]
636mod tests {
637 #[test]
638 fn test_source_rpm_purl_decomposes_a_nevra_filename() {
639 assert_eq!(
642 super::source_rpm_purl("gcc-13.1.1-2.fc38.src.rpm").as_deref(),
643 Some("pkg:rpm/gcc@13.1.1-2.fc38?arch=src")
644 );
645 assert_eq!(
648 super::source_rpm_purl("fedora-modular-repos-26-0.4.module_39876f37.src.rpm")
649 .as_deref(),
650 Some("pkg:rpm/fedora-modular-repos@26-0.4.module_39876f37?arch=src")
651 );
652 assert_eq!(
653 super::source_rpm_purl("fping-2.4b2-10.fc12.src.rpm").as_deref(),
654 Some("pkg:rpm/fping@2.4b2-10.fc12?arch=src")
655 );
656
657 for malformed in ["gcc.rpm", "gcc-13.src.rpm", "not-an-rpm", ""] {
660 assert_eq!(super::source_rpm_purl(malformed), None, "{malformed:?}");
661 }
662 }
663
664 use super::*;
665
666 use crate::models::DatasourceId;
667 use std::path::PathBuf;
668
669 #[test]
670 fn test_bdb_parser_is_match() {
671 assert!(RpmBdbDatabaseParser::is_match(&PathBuf::from(
672 "/var/lib/rpm/Packages"
673 )));
674 assert!(RpmBdbDatabaseParser::is_match(&PathBuf::from(
675 "rootfs/var/lib/rpm/Packages"
676 )));
677 assert!(RpmBdbDatabaseParser::is_match(&PathBuf::from(
678 "/usr/lib/sysimage/rpm/Packages"
679 )));
680 assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from(
681 "/var/lib/rpm/Packages.db"
682 )));
683 assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from(
684 "lib/modules/datasource/deb/__fixtures__/Packages"
685 )));
686 assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from("Packages")));
687 assert!(!RpmBdbDatabaseParser::is_match(&PathBuf::from(
688 "testdata/rpm/var/lib/rpm/Packages.expected.json"
689 )));
690 }
691
692 #[test]
693 fn test_ndb_parser_is_match() {
694 assert!(RpmNdbDatabaseParser::is_match(&PathBuf::from(
695 "usr/lib/sysimage/rpm/Packages.db"
696 )));
697 assert!(RpmNdbDatabaseParser::is_match(&PathBuf::from(
698 "/rootfs/usr/lib/sysimage/rpm/Packages.db"
699 )));
700 assert!(!RpmNdbDatabaseParser::is_match(&PathBuf::from(
701 "usr/lib/rpm/Packages"
702 )));
703 assert!(RpmNdbDatabaseParser::is_match(&PathBuf::from(
704 "var/lib/rpm/Packages.db"
705 )));
706 assert!(!RpmNdbDatabaseParser::is_match(&PathBuf::from(
707 "testdata/rpm/usr/lib/sysimage/rpm/Packages.db.expected.json"
708 )));
709 }
710
711 #[cfg(feature = "rpm-sqlite")]
712 #[test]
713 fn test_sqlite_parser_is_match() {
714 assert!(RpmSqliteDatabaseParser::is_match(&PathBuf::from(
715 "var/lib/rpm/rpmdb.sqlite"
716 )));
717 assert!(RpmSqliteDatabaseParser::is_match(&PathBuf::from(
718 "/rootfs/var/lib/rpm/rpmdb.sqlite"
719 )));
720 assert!(RpmSqliteDatabaseParser::is_match(&PathBuf::from(
721 "/rootfs/usr/lib/sysimage/rpm/rpmdb.sqlite"
722 )));
723 assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
724 "/var/lib/rpm/Packages"
725 )));
726 assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
727 "testdata/rpm/rpmdb.sqlite.expected.json"
728 )));
729 assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
730 "testdata/rpm/rpmdb.sqlite-shm"
731 )));
732 assert!(!RpmSqliteDatabaseParser::is_match(&PathBuf::from(
733 "testdata/rpm/rpmdb.sqlite-wal"
734 )));
735 }
736
737 #[test]
738 fn test_build_version_release_full() {
739 assert_eq!(
741 build_version_release("1.0.0", "1.el7"),
742 Some("1.0.0-1.el7".to_string())
743 );
744 }
745
746 #[test]
747 fn test_build_version_release_no_release() {
748 assert_eq!(
749 build_version_release("1.0.0", ""),
750 Some("1.0.0".to_string())
751 );
752 }
753
754 #[test]
755 fn test_build_version_release_empty() {
756 assert_eq!(build_version_release("", ""), None);
757 }
758
759 #[cfg(feature = "rpm-sqlite")]
760 #[test]
761 fn test_parse_rpm_database_sqlite() {
762 let test_file = PathBuf::from("testdata/rpm/rpmdb.sqlite");
763
764 let pkg = RpmSqliteDatabaseParser::extract_first_package(&test_file);
765
766 assert_eq!(pkg.package_type, Some(PackageType::Rpm));
767 assert_eq!(
768 pkg.datasource_id,
769 Some(DatasourceId::RpmInstalledDatabaseSqlite)
770 );
771 assert!(pkg.name.is_some());
772 }
773
774 #[cfg(feature = "rpm-sqlite")]
775 #[test]
776 fn test_parse_rpm_database_sqlite_preserves_release_in_version() {
777 let test_file = PathBuf::from("testdata/rpm/rpmdb.sqlite");
778
779 let pkg = RpmSqliteDatabaseParser::extract_first_package(&test_file);
780
781 assert!(
782 pkg.version
783 .as_ref()
784 .is_some_and(|version| version.contains('-'))
785 );
786 }
787
788 #[test]
789 fn test_build_file_references_skips_invalid_entries() {
790 let file_refs = build_file_references(
791 &[
792 Some("valid".to_string()),
793 Some("".to_string()),
794 Some("ignored".to_string()),
795 ],
796 &[0, 0, u32::MAX],
797 &["/usr/bin/".to_string()],
798 );
799
800 assert_eq!(file_refs.len(), 2);
801 assert_eq!(file_refs[0].path, "/usr/bin/valid");
802 assert_eq!(file_refs[1].path, "/usr/bin/");
803 }
804
805 #[test]
806 fn test_build_package_data_falls_back_to_file_names() {
807 let package = build_package_data(
808 RpmQueryPackage {
809 name: Some("libgcc".to_string()),
810 epoch: None,
811 version: Some("13.1.1".to_string()),
812 release: Some("2.fc38".to_string()),
813 vendor: Some("Fedora Project".to_string()),
814 distribution: None,
815 arch: Some("x86_64".to_string()),
816 platform: None,
817 size: Some(235748),
818 license: Some("GPLv3+".to_string()),
819 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
820 requires: Vec::new(),
821 file_names: vec![
822 Some("/usr/share/licenses/libgcc/COPYING".to_string()),
823 Some("/usr/share/licenses/libgcc/COPYING.RUNTIME".to_string()),
824 ],
825 dir_indexes: Vec::new(),
826 base_names: Vec::new(),
827 dir_names: Vec::new(),
828 },
829 DatasourceId::RpmInstalledDatabaseSqlite,
830 );
831
832 assert_eq!(package.file_references.len(), 2);
833 assert_eq!(
834 package.file_references[0].path,
835 "/usr/share/licenses/libgcc/COPYING"
836 );
837 assert_eq!(
838 package.file_references[1].path,
839 "/usr/share/licenses/libgcc/COPYING.RUNTIME"
840 );
841 }
842
843 #[test]
844 fn test_build_package_data_uses_distribution_for_namespace() {
845 let package = build_package_data(
846 RpmQueryPackage {
847 name: Some("libgcc".to_string()),
848 epoch: None,
849 version: Some("13.1.1".to_string()),
850 release: Some("2.fc38".to_string()),
851 vendor: None,
852 distribution: Some("Fedora Project".to_string()),
853 arch: Some("x86_64".to_string()),
854 platform: None,
855 size: Some(235748),
856 license: Some("GPLv3+".to_string()),
857 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
858 requires: Vec::new(),
859 file_names: vec![Some("/usr/share/licenses/libgcc/COPYING".to_string())],
860 dir_indexes: Vec::new(),
861 base_names: Vec::new(),
862 dir_names: Vec::new(),
863 },
864 DatasourceId::RpmInstalledDatabaseSqlite,
865 );
866
867 assert_eq!(package.namespace.as_deref(), Some("fedora"));
868 }
869
870 #[test]
871 fn test_build_package_data_normalizes_declared_license_expression() {
872 let package = build_package_data(
873 RpmQueryPackage {
874 name: Some("libgcc".to_string()),
875 epoch: None,
876 version: Some("13.1.1".to_string()),
877 release: Some("2.fc38".to_string()),
878 vendor: Some("Fedora Project".to_string()),
879 distribution: None,
880 arch: Some("x86_64".to_string()),
881 platform: None,
882 size: Some(235748),
883 license: Some("LGPLv2".to_string()),
884 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
885 requires: Vec::new(),
886 file_names: Vec::new(),
887 dir_indexes: Vec::new(),
888 base_names: Vec::new(),
889 dir_names: Vec::new(),
890 },
891 DatasourceId::RpmInstalledDatabaseSqlite,
892 );
893
894 assert_eq!(
895 package.declared_license_expression.as_deref(),
896 Some("lgpl-2.0")
897 );
898 assert_eq!(
899 package.declared_license_expression_spdx.as_deref(),
900 Some("LGPL-2.0-only")
901 );
902 assert_eq!(package.license_detections.len(), 1);
903 }
904
905 #[test]
906 fn test_build_package_data_uses_source_rpm_for_namespace() {
907 let package = build_package_data(
908 RpmQueryPackage {
909 name: Some("libgcc".to_string()),
910 epoch: None,
911 version: Some("13.1.1".to_string()),
912 release: None,
913 vendor: None,
914 distribution: None,
915 arch: Some("x86_64".to_string()),
916 platform: None,
917 size: Some(235748),
918 license: Some("GPLv3+".to_string()),
919 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
920 requires: Vec::new(),
921 file_names: vec![Some("/usr/share/licenses/libgcc/COPYING".to_string())],
922 dir_indexes: Vec::new(),
923 base_names: Vec::new(),
924 dir_names: Vec::new(),
925 },
926 DatasourceId::RpmInstalledDatabaseSqlite,
927 );
928
929 assert_eq!(package.namespace.as_deref(), Some("fedora"));
930 }
931
932 #[test]
933 fn test_build_package_data_uses_platform_for_architecture() {
934 let package = build_package_data(
935 RpmQueryPackage {
936 name: Some("libgcc".to_string()),
937 epoch: None,
938 version: Some("13.1.1".to_string()),
939 release: None,
940 vendor: None,
941 distribution: None,
942 arch: None,
943 platform: Some("x86_64-redhat-linux".to_string()),
944 size: Some(235748),
945 license: Some("GPLv3+".to_string()),
946 source_rpm: Some("gcc-13.1.1-2.fc38.src.rpm".to_string()),
947 requires: Vec::new(),
948 file_names: vec![Some("/usr/share/licenses/libgcc/COPYING".to_string())],
949 dir_indexes: Vec::new(),
950 base_names: Vec::new(),
951 dir_names: Vec::new(),
952 },
953 DatasourceId::RpmInstalledDatabaseSqlite,
954 );
955
956 assert_eq!(
957 package.qualifiers.as_ref().and_then(|q| q.get("arch")),
958 Some(&"x86_64".to_string())
959 );
960 }
961}