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