1use std::fs::File;
23use std::io::{BufReader, Read};
24use std::path::Path;
25
26use log::warn;
27use rpm::{IndexTag, Package, PackageMetadata, RPM_MAGIC};
28
29use crate::models::{DatasourceId, Dependency, PackageData, PackageType, Party};
30
31use super::PackageParser;
32
33const PACKAGE_TYPE: PackageType = PackageType::Rpm;
34
35fn default_package_data() -> PackageData {
36 PackageData {
37 package_type: Some(PACKAGE_TYPE),
38 datasource_id: Some(DatasourceId::RpmArchive),
39 ..Default::default()
40 }
41}
42
43pub(crate) fn infer_rpm_namespace(
44 distribution: Option<&str>,
45 vendor: Option<&str>,
46 release: Option<&str>,
47 dist_url: Option<&str>,
48) -> Option<String> {
49 for candidate in [distribution, vendor, dist_url].into_iter().flatten() {
50 let lower = candidate.to_ascii_lowercase();
51 if lower.contains("fedora") || lower.contains("koji") {
52 return Some("fedora".to_string());
53 }
54 if lower.contains("centos") {
55 return Some("centos".to_string());
56 }
57 if lower.contains("red hat") || lower.contains("redhat") || lower.contains("ubi") {
58 return Some("rhel".to_string());
59 }
60 if lower.contains("opensuse") {
61 return Some("opensuse".to_string());
62 }
63 if lower.contains("suse") {
64 return Some("suse".to_string());
65 }
66 if lower.contains("openmandriva") || lower.contains("mandriva") {
67 return Some("openmandriva".to_string());
68 }
69 if lower.contains("mariner") {
70 return Some("mariner".to_string());
71 }
72 }
73
74 if let Some(release) = release {
75 let lower = release.to_ascii_lowercase();
76 if lower.contains(".fc") {
77 return Some("fedora".to_string());
78 }
79 if lower.contains(".el") {
80 return Some("rhel".to_string());
81 }
82 if lower.contains("mdv") || lower.contains("mnb") {
83 return Some("openmandriva".to_string());
84 }
85 if lower.contains("suse") {
86 return Some("suse".to_string());
87 }
88 }
89
90 None
91}
92
93fn rpm_header_string(metadata: &PackageMetadata, tag: IndexTag) -> Option<String> {
94 metadata
95 .header
96 .get_entry_data_as_string(tag)
97 .ok()
98 .and_then(|value| {
99 let trimmed = value.trim();
100 if trimmed.is_empty() || trimmed == "(none)" {
101 None
102 } else {
103 Some(trimmed.to_string())
104 }
105 })
106}
107
108fn rpm_header_string_array(metadata: &PackageMetadata, tag: IndexTag) -> Option<Vec<String>> {
109 metadata
110 .header
111 .get_entry_data_as_string_array(tag)
112 .ok()
113 .map(|items| {
114 items
115 .iter()
116 .map(|item| item.trim().to_string())
117 .filter(|item| !item.is_empty() && item != "(none)")
118 .collect::<Vec<_>>()
119 })
120 .filter(|items| !items.is_empty())
121}
122
123fn infer_vcs_url(metadata: &PackageMetadata, source_urls: &[String]) -> Option<String> {
124 if let Ok(vcs) = metadata.get_vcs()
125 && !vcs.trim().is_empty()
126 {
127 return Some(vcs.to_string());
128 }
129
130 source_urls
131 .iter()
132 .find(|url| url.starts_with("git+") || url.contains("src.fedoraproject.org"))
133 .cloned()
134}
135
136fn build_rpm_qualifiers(
137 architecture: Option<&str>,
138 is_source: bool,
139) -> Option<std::collections::HashMap<String, String>> {
140 let mut qualifiers = std::collections::HashMap::new();
141
142 if let Some(arch) = architecture.filter(|arch| !arch.is_empty()) {
143 qualifiers.insert("arch".to_string(), arch.to_string());
144 }
145
146 if is_source {
147 qualifiers.insert("source".to_string(), "true".to_string());
148 }
149
150 (!qualifiers.is_empty()).then_some(qualifiers)
151}
152
153pub struct RpmParser;
155
156impl PackageParser for RpmParser {
157 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
158
159 fn is_match(path: &Path) -> bool {
160 if let Some(ext) = path.extension().and_then(|e| e.to_str())
161 && matches!(ext, "rpm" | "srpm")
162 {
163 return true;
164 }
165
166 let mut file = match File::open(path) {
167 Ok(file) => file,
168 Err(_) => return false,
169 };
170 let mut magic = [0_u8; 4];
171 file.read_exact(&mut magic).is_ok() && magic == RPM_MAGIC
172 }
173
174 fn extract_packages(path: &Path) -> Vec<PackageData> {
175 let file = match File::open(path) {
176 Ok(f) => f,
177 Err(e) => {
178 warn!("Failed to open RPM file {:?}: {}", path, e);
179 return vec![default_package_data()];
180 }
181 };
182
183 let mut reader = BufReader::new(file);
184 let pkg = match Package::parse(&mut reader) {
185 Ok(p) => p,
186 Err(e) => {
187 warn!("Failed to parse RPM file {:?}: {}", path, e);
188 return vec![default_package_data()];
189 }
190 };
191
192 vec![parse_rpm_package(&pkg, path)]
193 }
194}
195
196fn infer_rpm_namespace_from_filename(path: &Path) -> Option<String> {
197 let filename = path.file_name()?.to_str()?.to_ascii_lowercase();
198
199 if filename.contains(".fc") {
200 return Some("fedora".to_string());
201 }
202 if filename.contains(".el") {
203 return Some("rhel".to_string());
204 }
205 if filename.contains("mdv") || filename.contains("mnb") {
206 return Some("openmandriva".to_string());
207 }
208 if filename.contains("opensuse") {
209 return Some("opensuse".to_string());
210 }
211 if filename.contains("suse") {
212 return Some("suse".to_string());
213 }
214
215 None
216}
217
218fn parse_rpm_package(pkg: &Package, path: &Path) -> PackageData {
219 let metadata = &pkg.metadata;
220
221 let name = metadata.get_name().ok().map(|s| s.to_string());
222 let version = build_evr_version(metadata);
223 let description = metadata.get_description().ok().map(|s| s.to_string());
224 let homepage_url = metadata.get_url().ok().map(|s| s.to_string());
225 let architecture = metadata.get_arch().ok().map(|s| s.to_string());
226 let path_str = path.to_string_lossy();
227 let is_source = metadata.is_source_package()
228 || path_str.ends_with(".src.rpm")
229 || path_str.ends_with(".srpm");
230 let distribution = rpm_header_string(metadata, IndexTag::RPMTAG_DISTRIBUTION);
231 let dist_url = rpm_header_string(metadata, IndexTag::RPMTAG_DISTURL);
232 let bug_tracking_url = rpm_header_string(metadata, IndexTag::RPMTAG_BUGURL);
233 let source_urls =
234 rpm_header_string_array(metadata, IndexTag::RPMTAG_SOURCE).unwrap_or_default();
235 let source_rpm = metadata
236 .get_source_rpm()
237 .ok()
238 .filter(|value| !value.is_empty())
239 .map(|value| value.to_string());
240 let namespace = infer_rpm_namespace(
241 distribution.as_deref(),
242 metadata.get_vendor().ok(),
243 metadata.get_release().ok(),
244 dist_url.as_deref(),
245 )
246 .or_else(|| infer_rpm_namespace_from_filename(path));
247
248 let mut parties = Vec::new();
249
250 if let Ok(vendor) = metadata.get_vendor()
251 && !vendor.is_empty()
252 {
253 parties.push(Party {
254 r#type: Some("organization".to_string()),
255 role: Some("vendor".to_string()),
256 name: Some(vendor.to_string()),
257 email: None,
258 url: None,
259 organization: None,
260 organization_url: None,
261 timezone: None,
262 });
263 }
264
265 if let Some(distribution_name) = distribution.as_ref() {
266 parties.push(Party {
267 r#type: Some("organization".to_string()),
268 role: Some("distributor".to_string()),
269 name: Some(distribution_name.clone()),
270 email: None,
271 url: None,
272 organization: None,
273 organization_url: None,
274 timezone: None,
275 });
276 }
277
278 if let Ok(packager) = metadata.get_packager()
279 && !packager.is_empty()
280 {
281 let (name_opt, email_opt) = parse_packager(packager);
282 parties.push(Party {
283 r#type: Some("person".to_string()),
284 role: Some("packager".to_string()),
285 name: name_opt,
286 email: email_opt,
287 url: None,
288 organization: None,
289 organization_url: None,
290 timezone: None,
291 });
292 }
293
294 let extracted_license_statement = metadata.get_license().ok().map(|s| s.to_string());
295
296 let dependencies = extract_rpm_dependencies(pkg, namespace.as_deref());
297
298 let qualifiers = build_rpm_qualifiers(architecture.as_deref(), is_source);
299
300 let mut keywords = Vec::new();
301 if let Ok(group) = metadata.get_group()
302 && !group.is_empty()
303 {
304 keywords.push(group.to_string());
305 }
306
307 let mut extra_data = std::collections::HashMap::new();
308 if let Some(distribution) = distribution.clone() {
309 extra_data.insert(
310 "distribution".to_string(),
311 serde_json::Value::String(distribution),
312 );
313 }
314 if let Some(dist_url) = dist_url.clone() {
315 extra_data.insert("dist_url".to_string(), serde_json::Value::String(dist_url));
316 }
317 if let Ok(build_host) = metadata.get_build_host()
318 && !build_host.is_empty()
319 {
320 extra_data.insert(
321 "build_host".to_string(),
322 serde_json::Value::String(build_host.to_string()),
323 );
324 }
325 if let Ok(build_time) = metadata.get_build_time() {
326 extra_data.insert(
327 "build_time".to_string(),
328 serde_json::Value::Number(serde_json::Number::from(build_time)),
329 );
330 }
331 if !source_urls.is_empty() {
332 extra_data.insert(
333 "source_urls".to_string(),
334 serde_json::Value::Array(
335 source_urls
336 .iter()
337 .cloned()
338 .map(serde_json::Value::String)
339 .collect(),
340 ),
341 );
342 }
343 if let Some(provides) = extract_rpm_relationships(pkg, RpmRelationshipKind::Provides)
344 && !provides.is_empty()
345 {
346 extra_data.insert(
347 "provides".to_string(),
348 serde_json::Value::Array(
349 provides
350 .into_iter()
351 .map(serde_json::Value::String)
352 .collect(),
353 ),
354 );
355 }
356 if let Some(obsoletes) = extract_rpm_relationships(pkg, RpmRelationshipKind::Obsoletes)
357 && !obsoletes.is_empty()
358 {
359 extra_data.insert(
360 "obsoletes".to_string(),
361 serde_json::Value::Array(
362 obsoletes
363 .into_iter()
364 .map(serde_json::Value::String)
365 .collect(),
366 ),
367 );
368 }
369 let vcs_url = infer_vcs_url(metadata, &source_urls);
370
371 PackageData {
372 datasource_id: Some(DatasourceId::RpmArchive),
373 package_type: Some(PACKAGE_TYPE),
374 namespace: namespace.clone(),
375 name: name.clone(),
376 version: version.clone(),
377 qualifiers,
378 description,
379 homepage_url,
380 size: metadata.get_installed_size().ok(),
381 parties,
382 keywords,
383 bug_tracking_url,
384 extracted_license_statement,
385 dependencies,
386 source_packages: source_rpm.into_iter().collect(),
387 vcs_url,
388 extra_data: (!extra_data.is_empty()).then_some(extra_data),
389 purl: name.as_ref().and_then(|n| {
390 build_rpm_purl(
391 n,
392 version.as_deref(),
393 namespace.as_deref(),
394 architecture.as_deref(),
395 is_source,
396 )
397 }),
398 ..Default::default()
399 }
400}
401
402fn extract_rpm_dependencies(pkg: &Package, namespace: Option<&str>) -> Vec<Dependency> {
403 let mut dependencies = Vec::new();
404
405 if let Ok(requires) = pkg.metadata.get_requires() {
406 for rpm_dep in requires {
407 let purl = build_rpm_purl(
408 &rpm_dep.name,
409 if rpm_dep.version.is_empty() {
410 None
411 } else {
412 Some(&rpm_dep.version)
413 },
414 namespace,
415 None,
416 false,
417 );
418
419 let extracted_requirement = if !rpm_dep.version.is_empty() {
420 Some(format_rpm_requirement(&rpm_dep))
421 } else {
422 None
423 };
424
425 dependencies.push(Dependency {
426 purl,
427 extracted_requirement,
428 scope: Some("install".to_string()),
429 is_runtime: Some(true),
430 is_optional: Some(false),
431 is_direct: Some(true),
432 resolved_package: None,
433 extra_data: None,
434 is_pinned: Some(!rpm_dep.version.is_empty()),
435 });
436 }
437 }
438
439 dependencies
440}
441
442enum RpmRelationshipKind {
443 Provides,
444 Obsoletes,
445}
446
447fn extract_rpm_relationships(pkg: &Package, kind: RpmRelationshipKind) -> Option<Vec<String>> {
448 let relationships = match kind {
449 RpmRelationshipKind::Provides => pkg.metadata.get_provides().ok()?,
450 RpmRelationshipKind::Obsoletes => pkg.metadata.get_obsoletes().ok()?,
451 };
452
453 let values: Vec<String> = relationships
454 .into_iter()
455 .map(|dep| format_rpm_requirement(&dep))
456 .filter(|value| !value.is_empty() && value != "(none)")
457 .collect();
458
459 (!values.is_empty()).then_some(values)
460}
461
462fn format_rpm_requirement(dep: &rpm::Dependency) -> String {
463 use rpm::DependencyFlags;
464
465 if dep.version.is_empty() {
466 return dep.name.clone();
467 }
468
469 let operator = if dep.flags.contains(DependencyFlags::EQUAL)
470 && dep.flags.contains(DependencyFlags::LESS)
471 {
472 "<="
473 } else if dep.flags.contains(DependencyFlags::EQUAL)
474 && dep.flags.contains(DependencyFlags::GREATER)
475 {
476 ">="
477 } else if dep.flags.contains(DependencyFlags::EQUAL) {
478 "="
479 } else if dep.flags.contains(DependencyFlags::LESS) {
480 "<"
481 } else if dep.flags.contains(DependencyFlags::GREATER) {
482 ">"
483 } else {
484 ""
485 };
486
487 if operator.is_empty() {
488 dep.name.clone()
489 } else {
490 format!("{} {} {}", dep.name, operator, dep.version)
491 }
492}
493
494fn build_evr_version(metadata: &PackageMetadata) -> Option<String> {
495 let version = metadata.get_version().ok()?;
496 let release = metadata.get_release().ok();
497
498 let mut evr = String::from(version);
499
500 if let Some(r) = release {
501 evr.push('-');
502 evr.push_str(r);
503 }
504
505 Some(evr)
506}
507
508fn parse_packager(packager: &str) -> (Option<String>, Option<String>) {
509 if let Some(email_start) = packager.find('<') {
510 let name = packager[..email_start].trim();
511 if let Some(email_end) = packager.find('>') {
512 let email = &packager[email_start + 1..email_end];
513 return (Some(name.to_string()), Some(email.to_string()));
514 }
515 }
516 (Some(packager.to_string()), None)
517}
518
519fn build_rpm_purl(
520 name: &str,
521 version: Option<&str>,
522 namespace: Option<&str>,
523 architecture: Option<&str>,
524 is_source: bool,
525) -> Option<String> {
526 use packageurl::PackageUrl;
527
528 let mut purl = PackageUrl::new(PACKAGE_TYPE.as_str(), name).ok()?;
529
530 if let Some(ns) = namespace {
531 purl.with_namespace(ns).ok()?;
532 }
533
534 if let Some(ver) = version {
535 purl.with_version(ver).ok()?;
536 }
537
538 if let Some(arch) = architecture {
539 purl.add_qualifier("arch", arch).ok()?;
540 }
541
542 if is_source {
543 purl.add_qualifier("source", "true").ok()?;
544 }
545
546 Some(purl.to_string())
547}
548
549#[cfg(test)]
550mod tests {
551 use super::*;
552 use std::fs;
553 use std::path::PathBuf;
554 use tempfile::NamedTempFile;
555
556 #[test]
557 fn test_rpm_parser_is_match() {
558 assert!(RpmParser::is_match(&PathBuf::from("package.rpm")));
559 assert!(RpmParser::is_match(&PathBuf::from("package.srpm")));
560 assert!(RpmParser::is_match(&PathBuf::from(
561 "test-1.0-1.el7.x86_64.rpm"
562 )));
563 assert!(!RpmParser::is_match(&PathBuf::from("package.deb")));
564 assert!(!RpmParser::is_match(&PathBuf::from("package.tar.gz")));
565 }
566
567 #[test]
568 fn test_rpm_parser_matches_hash_named_source_rpm_by_magic() {
569 let source_fixture = PathBuf::from("testdata/rpm/setup-2.5.49-b1.src.rpm");
570 if !source_fixture.exists() {
571 return;
572 }
573
574 let temp_file = NamedTempFile::new().unwrap();
575 fs::copy(&source_fixture, temp_file.path()).unwrap();
576
577 assert!(RpmParser::is_match(temp_file.path()));
578 }
579
580 #[test]
581 fn test_build_evr_version_simple() {
582 let evr = "1.0-1";
583 assert_eq!(evr, "1.0-1");
584 }
585
586 #[test]
587 fn test_build_evr_version_with_epoch() {
588 let evr = "2:1.0-1";
589 assert!(evr.starts_with("2:"));
590 }
591
592 #[test]
593 fn test_parse_packager() {
594 let (name, email) = parse_packager("John Doe <john@example.com>");
595 assert_eq!(name, Some("John Doe".to_string()));
596 assert_eq!(email, Some("john@example.com".to_string()));
597
598 let (name2, email2) = parse_packager("Plain Name");
599 assert_eq!(name2, Some("Plain Name".to_string()));
600 assert_eq!(email2, None);
601 }
602
603 #[test]
604 fn test_build_rpm_purl() {
605 let purl = build_rpm_purl(
606 "bash",
607 Some("4.4.19-1.el7"),
608 Some("fedora"),
609 Some("x86_64"),
610 false,
611 );
612 assert!(purl.is_some());
613 let purl_str = purl.unwrap();
614 assert!(purl_str.contains("pkg:rpm/fedora/bash"));
615 assert!(purl_str.contains("4.4.19-1.el7"));
616 assert!(purl_str.contains("arch=x86_64"));
617 }
618
619 #[test]
620 fn test_parse_real_rpm() {
621 let test_file = PathBuf::from("testdata/rpm/Eterm-0.9.3-5mdv2007.0.rpm");
622 if !test_file.exists() {
623 eprintln!("Warning: Test file not found, skipping test");
624 return;
625 }
626
627 let pkg = RpmParser::extract_first_package(&test_file);
628
629 assert_eq!(pkg.package_type, Some(PackageType::Rpm));
630
631 if pkg.name.is_some() {
632 assert_eq!(pkg.name, Some("Eterm".to_string()));
633 assert!(pkg.version.is_some());
634 }
635 }
636
637 #[test]
638 fn test_build_rpm_purl_no_namespace() {
639 let purl = build_rpm_purl("package", Some("1.0-1"), None, Some("x86_64"), false);
640 assert!(purl.is_some());
641 let purl_str = purl.unwrap();
642 assert!(purl_str.starts_with("pkg:rpm/package@"));
643 assert!(purl_str.contains("arch=x86_64"));
644 }
645
646 #[test]
647 fn test_rpm_dependency_extraction() {
648 use rpm::{Dependency as RpmDependency, DependencyFlags};
649
650 let rpm_dep = RpmDependency {
651 name: "libc.so.6".to_string(),
652 flags: DependencyFlags::GREATER | DependencyFlags::EQUAL,
653 version: "2.2.5".to_string(),
654 };
655
656 let formatted = format_rpm_requirement(&rpm_dep);
657 assert_eq!(formatted, "libc.so.6 >= 2.2.5");
658
659 let rpm_dep_no_version = RpmDependency {
660 name: "bash".to_string(),
661 flags: DependencyFlags::ANY,
662 version: String::new(),
663 };
664
665 let formatted_no_ver = format_rpm_requirement(&rpm_dep_no_version);
666 assert_eq!(formatted_no_ver, "bash");
667 }
668
669 #[test]
670 fn test_parse_packager_with_parentheses() {
671 let (name, email) = parse_packager("John Doe (Company) <john@example.com>");
672 assert_eq!(name, Some("John Doe (Company)".to_string()));
673 assert_eq!(email, Some("john@example.com".to_string()));
674 }
675
676 #[test]
677 fn test_parse_packager_email_only() {
678 let (name, email) = parse_packager("<noreply@example.com>");
679 assert!(name.is_none() || name == Some(String::new()));
680 assert_eq!(email, Some("noreply@example.com".to_string()));
681 }
682
683 #[test]
684 fn test_rpm_fping_package() {
685 let test_file = PathBuf::from("testdata/rpm/fping-2.4b2-10.fc12.x86_64.rpm");
686 if !test_file.exists() {
687 return;
688 }
689
690 let pkg = RpmParser::extract_first_package(&test_file);
691 if pkg.name.is_some() {
692 assert_eq!(pkg.name, Some("fping".to_string()));
693 assert!(pkg.version.is_some());
694 }
695 }
696
697 #[test]
698 fn test_rpm_archive_extracts_additional_metadata_fields() {
699 let test_file = PathBuf::from("testdata/rpm/setup-2.5.49-b1.src.rpm");
700 if !test_file.exists() {
701 return;
702 }
703
704 let pkg = RpmParser::extract_first_package(&test_file);
705
706 assert_eq!(pkg.name.as_deref(), Some("setup"));
707 assert_eq!(
708 pkg.qualifiers
709 .as_ref()
710 .and_then(|q| q.get("arch"))
711 .map(String::as_str),
712 Some("noarch")
713 );
714 assert!(!pkg.keywords.is_empty());
715 assert!(pkg.size.is_some());
716 assert!(
717 pkg.parties
718 .iter()
719 .any(|party| party.role.as_deref() == Some("packager"))
720 );
721 assert!(
722 pkg.qualifiers
723 .as_ref()
724 .is_some_and(|q| q.get("source") == Some(&"true".to_string()))
725 );
726 }
727
728 #[test]
729 fn test_source_rpm_sets_source_qualifier() {
730 let test_file = PathBuf::from("testdata/rpm/setup-2.5.49-b1.src.rpm");
731 if !test_file.exists() {
732 return;
733 }
734
735 let pkg = RpmParser::extract_first_package(&test_file);
736
737 assert!(
738 pkg.qualifiers
739 .as_ref()
740 .is_some_and(|q| q.get("source") == Some(&"true".to_string()))
741 );
742 assert!(
743 pkg.purl
744 .as_ref()
745 .is_some_and(|purl| purl.contains("source=true"))
746 );
747 }
748
749 #[test]
750 fn test_rpm_archive_extracts_vcs_and_source_metadata() {
751 let package = rpm::PackageBuilder::new(
752 "thunar-sendto-clamtk",
753 "0.08",
754 "GPL-2.0-or-later",
755 "noarch",
756 "Simple virus scanning extension for Thunar",
757 )
758 .release("2.fc40")
759 .vendor("Fedora Project")
760 .packager("Fedora Release Engineering <releng@fedoraproject.org>")
761 .group("Applications/System")
762 .vcs("git+https://src.fedoraproject.org/rpms/thunar-sendto-clamtk.git#5a3f8e92b45f46b464e6924c79d4bf3e11bb1f0e")
763 .build()
764 .unwrap();
765
766 let temp_file = NamedTempFile::new().unwrap();
767 package.write_file(temp_file.path()).unwrap();
768
769 let pkg = RpmParser::extract_first_package(temp_file.path());
770
771 assert_eq!(pkg.namespace.as_deref(), Some("fedora"));
772 assert_eq!(
773 pkg.vcs_url.as_deref(),
774 Some(
775 "git+https://src.fedoraproject.org/rpms/thunar-sendto-clamtk.git#5a3f8e92b45f46b464e6924c79d4bf3e11bb1f0e",
776 )
777 );
778 assert!(
779 pkg.extra_data
780 .as_ref()
781 .is_some_and(|extra| extra.contains_key("build_time"))
782 );
783 assert!(!pkg.keywords.is_empty());
784 }
785
786 #[test]
787 fn test_rpm_archive_preserves_provides_and_obsoletes_relationships() {
788 use rpm::{Dependency as RpmDependency, DependencyFlags};
789
790 let package = rpm::PackageBuilder::new(
791 "demo-rpm",
792 "1.0.0",
793 "MIT",
794 "noarch",
795 "RPM relationship metadata fixture",
796 )
797 .release("1")
798 .provides(RpmDependency {
799 name: "demo-rpm-virtual".to_string(),
800 flags: DependencyFlags::GREATER | DependencyFlags::EQUAL,
801 version: "1.0.0".to_string(),
802 })
803 .obsoletes(RpmDependency {
804 name: "old-demo-rpm".to_string(),
805 flags: DependencyFlags::LESS,
806 version: "0.9.0".to_string(),
807 })
808 .build()
809 .unwrap();
810
811 let temp_file = NamedTempFile::new().unwrap();
812 package.write_file(temp_file.path()).unwrap();
813
814 let pkg = RpmParser::extract_first_package(temp_file.path());
815 let extra = pkg.extra_data.as_ref().expect("extra_data should exist");
816
817 let provides = extra
818 .get("provides")
819 .and_then(|value| value.as_array())
820 .expect("provides should be present");
821 assert!(
822 provides
823 .iter()
824 .any(|value| value.as_str() == Some("demo-rpm-virtual >= 1.0.0"))
825 );
826
827 let obsoletes = extra
828 .get("obsoletes")
829 .and_then(|value| value.as_array())
830 .expect("obsoletes should be present");
831 assert!(
832 obsoletes
833 .iter()
834 .any(|value| value.as_str() == Some("old-demo-rpm < 0.9.0"))
835 );
836 }
837}
838
839crate::register_parser!(
840 "RPM package archive",
841 &["**/*.rpm", "**/*.srpm"],
842 "rpm",
843 "",
844 Some("https://rpm.org/"),
845);