1use std::collections::HashMap;
5use std::path::Path;
6
7use crate::models::{DatasourceId, PackageData, PackageType, Party};
8use crate::parser_warn as warn;
9use crate::parsers::rfc822::{self, Rfc822Metadata};
10use crate::parsers::utils::{MAX_ITERATION_COUNT, split_name_email, truncate_field};
11
12use super::utils::{
13 build_debian_purl, detect_namespace, make_party, parse_all_dependencies, parse_source_field,
14};
15use super::{PACKAGE_TYPE, default_package_data, read_or_default};
16use crate::parsers::PackageParser;
17
18pub struct DebianControlParser;
23
24impl PackageParser for DebianControlParser {
25 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
26
27 fn is_match(path: &Path) -> bool {
28 if let Some(name) = path.file_name()
29 && name == "control"
30 && let Some(parent) = path.parent()
31 && let Some(parent_name) = parent.file_name()
32 {
33 return parent_name == "debian";
34 }
35 false
36 }
37
38 fn extract_packages(path: &Path) -> Vec<PackageData> {
39 let content = read_or_default!(path, "debian/control", DatasourceId::DebianControlInSource);
40
41 let packages = parse_debian_control(&content);
42 if packages.is_empty() {
43 vec![default_package_data(DatasourceId::DebianControlInSource)]
44 } else {
45 packages
46 }
47 }
48}
49
50pub struct DebianInstalledParser;
55
56impl PackageParser for DebianInstalledParser {
57 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
58
59 fn is_match(path: &Path) -> bool {
60 let path_str = path.to_string_lossy();
61 path_str.ends_with("var/lib/dpkg/status")
62 }
63
64 fn extract_packages(path: &Path) -> Vec<PackageData> {
65 let content = read_or_default!(path, "dpkg/status", DatasourceId::DebianInstalledStatusDb);
66
67 let packages = parse_dpkg_status(&content);
68 if packages.is_empty() {
69 vec![default_package_data(DatasourceId::DebianInstalledStatusDb)]
70 } else {
71 packages
72 }
73 }
74}
75
76pub struct DebianDistrolessInstalledParser;
77
78impl PackageParser for DebianDistrolessInstalledParser {
79 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
80
81 fn is_match(path: &Path) -> bool {
82 let path_str = path.to_string_lossy();
83 path_str.contains("var/lib/dpkg/status.d/")
84 }
85
86 fn extract_packages(path: &Path) -> Vec<PackageData> {
87 let content = read_or_default!(
88 path,
89 "distroless status file",
90 DatasourceId::DebianDistrolessInstalledDb
91 );
92
93 vec![parse_distroless_status(&content)]
94 }
95}
96
97fn parse_distroless_status(content: &str) -> PackageData {
98 let paragraphs = rfc822::parse_rfc822_paragraphs(content);
99
100 if paragraphs.is_empty() {
101 return default_package_data(DatasourceId::DebianDistrolessInstalledDb);
102 }
103
104 build_package_from_paragraph(
105 ¶graphs[0],
106 None,
107 DatasourceId::DebianDistrolessInstalledDb,
108 )
109 .unwrap_or_else(|| default_package_data(DatasourceId::DebianDistrolessInstalledDb))
110}
111
112fn parse_debian_control(content: &str) -> Vec<PackageData> {
122 let paragraphs = rfc822::parse_rfc822_paragraphs(content);
123 if paragraphs.is_empty() {
124 return Vec::new();
125 }
126
127 let has_source = rfc822::get_header_first(¶graphs[0].headers, "source").is_some();
128
129 let (source_paragraph, binary_start) = if has_source {
130 (Some(¶graphs[0]), 1)
131 } else {
132 (None, 0)
133 };
134
135 let source_meta = source_paragraph.map(extract_source_meta);
136
137 let mut packages = Vec::new();
138 let mut count = 0usize;
139
140 for para in ¶graphs[binary_start..] {
141 count += 1;
142 if count > MAX_ITERATION_COUNT {
143 warn!("parse_debian_control: exceeded MAX_ITERATION_COUNT paragraphs, stopping");
144 break;
145 }
146 if let Some(pkg) = build_package_from_paragraph(
147 para,
148 source_meta.as_ref(),
149 DatasourceId::DebianControlInSource,
150 ) {
151 packages.push(pkg);
152 }
153 }
154
155 if packages.is_empty()
156 && let Some(source_para) = source_paragraph
157 && let Some(pkg) = build_package_from_source_paragraph(source_para)
158 {
159 packages.push(pkg);
160 }
161
162 packages
163}
164
165fn parse_dpkg_status(content: &str) -> Vec<PackageData> {
170 let paragraphs = rfc822::parse_rfc822_paragraphs(content);
171 let mut packages = Vec::new();
172 let mut count = 0usize;
173
174 for para in ¶graphs {
175 count += 1;
176 if count > MAX_ITERATION_COUNT {
177 warn!("parse_dpkg_status: exceeded MAX_ITERATION_COUNT paragraphs, stopping");
178 break;
179 }
180 let status = rfc822::get_header_first(¶.headers, "status");
181 if status.as_deref() != Some("install ok installed") {
182 continue;
183 }
184
185 if let Some(pkg) =
186 build_package_from_paragraph(para, None, DatasourceId::DebianInstalledStatusDb)
187 {
188 packages.push(pkg);
189 }
190 }
191
192 packages
193}
194
195pub(super) struct SourceMeta {
200 parties: Vec<Party>,
201 homepage_url: Option<String>,
202 vcs_url: Option<String>,
203 code_view_url: Option<String>,
204 bug_tracking_url: Option<String>,
205}
206
207fn extract_source_meta(paragraph: &Rfc822Metadata) -> SourceMeta {
208 let mut parties = Vec::new();
209
210 if let Some(maintainer) = rfc822::get_header_first(¶graph.headers, "maintainer") {
212 let (name, email) = split_name_email(&maintainer);
213 parties.push(make_party(Some("person"), "maintainer", name, email));
214 }
215
216 if let Some(orig_maintainer) =
218 rfc822::get_header_first(¶graph.headers, "original-maintainer")
219 {
220 let (name, email) = split_name_email(&orig_maintainer);
221 parties.push(make_party(Some("person"), "maintainer", name, email));
222 }
223
224 if let Some(uploaders_str) = rfc822::get_header_first(¶graph.headers, "uploaders") {
226 for uploader in uploaders_str.split(',') {
227 let trimmed = uploader.trim();
228 if !trimmed.is_empty() {
229 let (name, email) = split_name_email(trimmed);
230 parties.push(make_party(Some("person"), "uploader", name, email));
231 }
232 }
233 }
234
235 let homepage_url = rfc822::get_header_first(¶graph.headers, "homepage").map(truncate_field);
236
237 let vcs_url = rfc822::get_header_first(¶graph.headers, "vcs-git")
238 .map(|url| truncate_field(url.split_whitespace().next().unwrap_or(&url).to_string()));
239
240 let code_view_url =
241 rfc822::get_header_first(¶graph.headers, "vcs-browser").map(truncate_field);
242
243 let bug_tracking_url = rfc822::get_header_first(¶graph.headers, "bugs").map(truncate_field);
244
245 SourceMeta {
246 parties,
247 homepage_url,
248 vcs_url,
249 code_view_url,
250 bug_tracking_url,
251 }
252}
253
254pub(super) fn build_package_from_paragraph(
259 paragraph: &Rfc822Metadata,
260 source_meta: Option<&SourceMeta>,
261 datasource_id: DatasourceId,
262) -> Option<PackageData> {
263 let name = rfc822::get_header_first(¶graph.headers, "package").map(truncate_field)?;
264 let version = rfc822::get_header_first(¶graph.headers, "version").map(truncate_field);
265 let architecture =
266 rfc822::get_header_first(¶graph.headers, "architecture").map(truncate_field);
267 let description =
268 rfc822::get_header_first(¶graph.headers, "description").map(truncate_field);
269 let maintainer_str = rfc822::get_header_first(¶graph.headers, "maintainer");
270 let homepage = rfc822::get_header_first(¶graph.headers, "homepage").map(truncate_field);
271 let source_field = rfc822::get_header_first(¶graph.headers, "source");
272 let section = rfc822::get_header_first(¶graph.headers, "section");
273 let installed_size = rfc822::get_header_first(¶graph.headers, "installed-size");
274 let multi_arch = rfc822::get_header_first(¶graph.headers, "multi-arch");
275
276 let namespace = detect_namespace(version.as_deref(), maintainer_str.as_deref());
277
278 let parties = if let Some(meta) = source_meta {
280 meta.parties.clone()
281 } else {
282 let mut p = Vec::new();
283 if let Some(m) = &maintainer_str {
284 let (n, e) = split_name_email(m);
285 p.push(make_party(Some("person"), "maintainer", n, e));
286 }
287 p
288 };
289
290 let homepage_url = homepage.or_else(|| source_meta.and_then(|m| m.homepage_url.clone()));
292 let vcs_url = source_meta.and_then(|m| m.vcs_url.clone());
293 let code_view_url = source_meta.and_then(|m| m.code_view_url.clone());
294 let bug_tracking_url = source_meta.and_then(|m| m.bug_tracking_url.clone());
295
296 let purl = build_debian_purl(
298 &name,
299 version.as_deref(),
300 namespace.as_deref(),
301 architecture.as_deref(),
302 );
303
304 let dependencies = parse_all_dependencies(¶graph.headers, namespace.as_deref());
306
307 let keywords = section.into_iter().collect();
309
310 let source_packages = parse_source_field(source_field.as_deref(), namespace.as_deref());
312
313 let mut extra_data: HashMap<String, serde_json::Value> = HashMap::new();
315 if let Some(ma) = &multi_arch
316 && !ma.is_empty()
317 {
318 extra_data.insert(
319 "multi_arch".to_string(),
320 serde_json::Value::String(ma.clone()),
321 );
322 }
323 if let Some(size_str) = &installed_size
324 && let Ok(size) = size_str.parse::<u64>()
325 {
326 extra_data.insert(
327 "installed_size".to_string(),
328 serde_json::Value::Number(serde_json::Number::from(size)),
329 );
330 }
331
332 let qualifiers = architecture.as_ref().map(|arch| {
334 let mut q = HashMap::new();
335 q.insert("arch".to_string(), arch.clone());
336 q
337 });
338
339 Some(PackageData {
340 package_type: Some(PACKAGE_TYPE),
341 namespace: namespace.clone(),
342 name: Some(name),
343 version,
344 qualifiers,
345 description,
346 parties,
347 keywords,
348 homepage_url,
349 bug_tracking_url,
350 code_view_url,
351 vcs_url,
352 source_packages,
353 file_references: Vec::new(),
354 extra_data: if extra_data.is_empty() {
355 None
356 } else {
357 Some(extra_data)
358 },
359 dependencies,
360 datasource_id: Some(datasource_id),
361 purl,
362 ..Default::default()
363 })
364}
365
366fn build_package_from_source_paragraph(paragraph: &Rfc822Metadata) -> Option<PackageData> {
367 let name = rfc822::get_header_first(¶graph.headers, "source").map(truncate_field)?;
368 let version = rfc822::get_header_first(¶graph.headers, "version").map(truncate_field);
369 let maintainer_str = rfc822::get_header_first(¶graph.headers, "maintainer");
370
371 let namespace = detect_namespace(version.as_deref(), maintainer_str.as_deref());
372 let source_meta = extract_source_meta(paragraph);
373
374 let purl = build_debian_purl(&name, version.as_deref(), namespace.as_deref(), None);
375 let dependencies = parse_all_dependencies(¶graph.headers, namespace.as_deref());
376
377 let section = rfc822::get_header_first(¶graph.headers, "section");
378 let keywords = section.into_iter().collect();
379
380 Some(PackageData {
381 package_type: Some(PACKAGE_TYPE),
382 namespace: namespace.clone(),
383 name: Some(name),
384 version,
385 parties: source_meta.parties,
386 keywords,
387 homepage_url: source_meta.homepage_url,
388 bug_tracking_url: source_meta.bug_tracking_url,
389 code_view_url: source_meta.code_view_url,
390 vcs_url: source_meta.vcs_url,
391 dependencies,
392 datasource_id: Some(DatasourceId::DebianControlInSource),
393 purl,
394 ..Default::default()
395 })
396}
397
398crate::register_parser!(
403 "Debian source package control file (debian/control)",
404 &["**/debian/control"],
405 "deb",
406 "",
407 Some("https://www.debian.org/doc/debian-policy/ch-controlfields.html"),
408);
409
410crate::register_parser!(
411 "Debian installed package database (dpkg status)",
412 &["**/var/lib/dpkg/status"],
413 "deb",
414 "",
415 Some("https://www.debian.org/doc/debian-policy/ch-controlfields.html"),
416);
417
418crate::register_parser!(
419 "Debian distroless package database (status.d)",
420 &["**/var/lib/dpkg/status.d/*"],
421 "deb",
422 "",
423 Some("https://www.debian.org/doc/debian-policy/ch-controlfields.html"),
424);
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429 use crate::models::DatasourceId;
430 use crate::models::PackageType;
431 use std::path::Path;
432 use std::path::PathBuf;
433
434 #[test]
435 fn test_parse_debian_control_source_and_binary() {
436 let content = "\
437Source: curl
438Section: web
439Priority: optional
440Maintainer: Alessandro Ghedini <ghedo@debian.org>
441Homepage: https://curl.se/
442Vcs-Browser: https://salsa.debian.org/debian/curl
443Vcs-Git: https://salsa.debian.org/debian/curl.git
444Build-Depends: debhelper (>= 12), libssl-dev
445
446Package: curl
447Architecture: amd64
448Depends: libc6 (>= 2.17), libcurl4 (= ${binary:Version})
449Description: command line tool for transferring data with URL syntax";
450
451 let packages = parse_debian_control(content);
452 assert_eq!(packages.len(), 1);
453
454 let pkg = &packages[0];
455 assert_eq!(pkg.name, Some("curl".to_string()));
456 assert_eq!(pkg.package_type, Some(PackageType::Deb));
457 assert_eq!(pkg.homepage_url, Some("https://curl.se/".to_string()));
458 assert_eq!(
459 pkg.vcs_url,
460 Some("https://salsa.debian.org/debian/curl.git".to_string())
461 );
462 assert_eq!(
463 pkg.code_view_url,
464 Some("https://salsa.debian.org/debian/curl".to_string())
465 );
466
467 assert_eq!(pkg.parties.len(), 1);
468 assert_eq!(pkg.parties[0].role, Some("maintainer".to_string()));
469 assert_eq!(pkg.parties[0].name, Some("Alessandro Ghedini".to_string()));
470 assert_eq!(pkg.parties[0].email, Some("ghedo@debian.org".to_string()));
471
472 assert!(!pkg.dependencies.is_empty());
473 }
474
475 #[test]
476 fn test_parse_debian_control_multiple_binary() {
477 let content = "\
478Source: gzip
479Maintainer: Debian Developer <dev@debian.org>
480
481Package: gzip
482Architecture: any
483Depends: libc6 (>= 2.17)
484Description: GNU file compression
485
486Package: gzip-win32
487Architecture: all
488Description: gzip for Windows";
489
490 let packages = parse_debian_control(content);
491 assert_eq!(packages.len(), 2);
492 assert_eq!(packages[0].name, Some("gzip".to_string()));
493 assert_eq!(packages[1].name, Some("gzip-win32".to_string()));
494
495 assert_eq!(packages[0].parties.len(), 1);
496 assert_eq!(packages[1].parties.len(), 1);
497 }
498
499 #[test]
500 fn test_parse_debian_control_source_only() {
501 let content = "\
502Source: my-package
503Maintainer: Test User <test@debian.org>
504Build-Depends: debhelper (>= 13)";
505
506 let packages = parse_debian_control(content);
507 assert_eq!(packages.len(), 1);
508 assert_eq!(packages[0].name, Some("my-package".to_string()));
509 assert!(!packages[0].dependencies.is_empty());
510 assert_eq!(
511 packages[0].dependencies[0].scope,
512 Some("build-depends".to_string())
513 );
514 }
515
516 #[test]
517 fn test_parse_debian_control_with_uploaders() {
518 let content = "\
519Source: example
520Maintainer: Main Dev <main@debian.org>
521Uploaders: Alice <alice@example.com>, Bob <bob@example.com>
522
523Package: example
524Architecture: any
525Description: test package";
526
527 let packages = parse_debian_control(content);
528 assert_eq!(packages.len(), 1);
529 assert_eq!(packages[0].parties.len(), 3);
530 assert_eq!(packages[0].parties[0].role, Some("maintainer".to_string()));
531 assert_eq!(packages[0].parties[1].role, Some("uploader".to_string()));
532 assert_eq!(packages[0].parties[2].role, Some("uploader".to_string()));
533 }
534
535 #[test]
536 fn test_parse_debian_control_vcs_git_with_branch() {
537 let content = "\
538Source: example
539Maintainer: Dev <dev@debian.org>
540Vcs-Git: https://salsa.debian.org/example.git -b main
541
542Package: example
543Architecture: any
544Description: test";
545
546 let packages = parse_debian_control(content);
547 assert_eq!(packages.len(), 1);
548 assert_eq!(
549 packages[0].vcs_url,
550 Some("https://salsa.debian.org/example.git".to_string())
551 );
552 }
553
554 #[test]
555 fn test_parse_debian_control_multi_arch() {
556 let content = "\
557Source: example
558Maintainer: Dev <dev@debian.org>
559
560Package: libexample
561Architecture: any
562Multi-Arch: same
563Description: shared library";
564
565 let packages = parse_debian_control(content);
566 assert_eq!(packages.len(), 1);
567 let extra = packages[0].extra_data.as_ref().unwrap();
568 assert_eq!(
569 extra.get("multi_arch"),
570 Some(&serde_json::Value::String("same".to_string()))
571 );
572 }
573
574 #[test]
575 fn test_parse_dpkg_status_basic() {
576 let content = "\
577Package: base-files
578Status: install ok installed
579Priority: required
580Section: admin
581Installed-Size: 391
582Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
583Architecture: amd64
584Version: 11ubuntu5.6
585Description: Debian base system miscellaneous files
586Homepage: https://tracker.debian.org/pkg/base-files
587
588Package: not-installed
589Status: deinstall ok config-files
590Architecture: amd64
591Version: 1.0
592Description: This should be skipped";
593
594 let packages = parse_dpkg_status(content);
595 assert_eq!(packages.len(), 1);
596
597 let pkg = &packages[0];
598 assert_eq!(pkg.name, Some("base-files".to_string()));
599 assert_eq!(pkg.version, Some("11ubuntu5.6".to_string()));
600 assert_eq!(pkg.namespace, Some("ubuntu".to_string()));
601 assert_eq!(
602 pkg.datasource_id,
603 Some(DatasourceId::DebianInstalledStatusDb)
604 );
605
606 let extra = pkg.extra_data.as_ref().unwrap();
607 assert_eq!(
608 extra.get("installed_size"),
609 Some(&serde_json::Value::Number(serde_json::Number::from(391)))
610 );
611 }
612
613 #[test]
614 fn test_parse_dpkg_status_multiple_installed() {
615 let content = "\
616Package: libc6
617Status: install ok installed
618Architecture: amd64
619Version: 2.31-13+deb11u5
620Maintainer: GNU Libc Maintainers <debian-glibc@lists.debian.org>
621Description: GNU C Library
622
623Package: zlib1g
624Status: install ok installed
625Architecture: amd64
626Version: 1:1.2.11.dfsg-2+deb11u2
627Maintainer: Mark Brown <broonie@debian.org>
628Description: compression library";
629
630 let packages = parse_dpkg_status(content);
631 assert_eq!(packages.len(), 2);
632 assert_eq!(packages[0].name, Some("libc6".to_string()));
633 assert_eq!(packages[1].name, Some("zlib1g".to_string()));
634 }
635
636 #[test]
637 fn test_parse_dpkg_status_with_dependencies() {
638 let content = "\
639Package: curl
640Status: install ok installed
641Architecture: amd64
642Version: 7.74.0-1.3+deb11u7
643Maintainer: Alessandro Ghedini <ghedo@debian.org>
644Depends: libc6 (>= 2.17), libcurl4 (= 7.74.0-1.3+deb11u7)
645Recommends: ca-certificates
646Description: command line tool for transferring data with URL syntax";
647
648 let packages = parse_dpkg_status(content);
649 assert_eq!(packages.len(), 1);
650
651 let deps = &packages[0].dependencies;
652 assert_eq!(deps.len(), 3);
653
654 assert_eq!(deps[0].purl, Some("pkg:deb/debian/libc6".to_string()));
655 assert_eq!(deps[0].scope, Some("depends".to_string()));
656 assert_eq!(deps[0].extracted_requirement, Some(">= 2.17".to_string()));
657
658 assert_eq!(
659 deps[2].purl,
660 Some("pkg:deb/debian/ca-certificates".to_string())
661 );
662 assert_eq!(deps[2].scope, Some("recommends".to_string()));
663 assert_eq!(deps[2].is_optional, Some(true));
664 }
665
666 #[test]
667 fn test_parse_dpkg_status_with_source() {
668 let content = "\
669Package: libncurses6
670Status: install ok installed
671Architecture: amd64
672Source: ncurses (6.2+20201114-2+deb11u1)
673Version: 6.2+20201114-2+deb11u1
674Maintainer: Craig Small <csmall@debian.org>
675Description: shared libraries for terminal handling";
676
677 let packages = parse_dpkg_status(content);
678 assert_eq!(packages.len(), 1);
679 assert!(!packages[0].source_packages.is_empty());
680 assert!(packages[0].source_packages[0].contains("ncurses"));
681 }
682
683 #[test]
684 fn test_parse_dpkg_status_filters_not_installed() {
685 let content = "\
686Package: installed-pkg
687Status: install ok installed
688Version: 1.0
689Architecture: amd64
690Description: installed
691
692Package: half-installed
693Status: install ok half-installed
694Version: 2.0
695Architecture: amd64
696Description: half installed
697
698Package: deinstall-pkg
699Status: deinstall ok config-files
700Version: 3.0
701Architecture: amd64
702Description: deinstalled
703
704Package: purge-pkg
705Status: purge ok not-installed
706Version: 4.0
707Architecture: amd64
708Description: purged";
709
710 let packages = parse_dpkg_status(content);
711 assert_eq!(packages.len(), 1);
712 assert_eq!(packages[0].name, Some("installed-pkg".to_string()));
713 }
714
715 #[test]
716 fn test_parse_dpkg_status_empty() {
717 let packages = parse_dpkg_status("");
718 assert!(packages.is_empty());
719 }
720
721 #[test]
722 fn test_debian_control_is_match() {
723 assert!(DebianControlParser::is_match(Path::new(
724 "/path/to/debian/control"
725 )));
726 assert!(DebianControlParser::is_match(Path::new("debian/control")));
727 assert!(!DebianControlParser::is_match(Path::new(
728 "/path/to/control"
729 )));
730 assert!(!DebianControlParser::is_match(Path::new(
731 "/path/to/debian/changelog"
732 )));
733 }
734
735 #[test]
736 fn test_debian_installed_is_match() {
737 assert!(DebianInstalledParser::is_match(Path::new(
738 "/var/lib/dpkg/status"
739 )));
740 assert!(DebianInstalledParser::is_match(Path::new(
741 "some/root/var/lib/dpkg/status"
742 )));
743 assert!(!DebianInstalledParser::is_match(Path::new(
744 "/var/lib/dpkg/status.d/something"
745 )));
746 assert!(!DebianInstalledParser::is_match(Path::new(
747 "/var/lib/dpkg/available"
748 )));
749 }
750
751 #[test]
752 fn test_parse_debian_control_empty_input() {
753 let packages = parse_debian_control("");
754 assert!(packages.is_empty());
755 }
756
757 #[test]
758 fn test_parse_debian_control_malformed_input() {
759 let content = "this is not a valid control file\nwith random text";
760 let packages = parse_debian_control(content);
761 assert!(packages.is_empty());
762 }
763
764 #[test]
765 fn test_distroless_parser() {
766 let test_file = PathBuf::from("testdata/debian/var/lib/dpkg/status.d/base-files");
767
768 assert!(DebianDistrolessInstalledParser::is_match(&test_file));
769
770 if !test_file.exists() {
771 eprintln!("Warning: Test file not found, skipping test");
772 return;
773 }
774
775 let pkg = DebianDistrolessInstalledParser::extract_first_package(&test_file);
776
777 assert_eq!(pkg.package_type, Some(PackageType::Deb));
778 assert_eq!(
779 pkg.datasource_id,
780 Some(DatasourceId::DebianDistrolessInstalledDb)
781 );
782 assert_eq!(pkg.name, Some("base-files".to_string()));
783 assert_eq!(pkg.version, Some("11.1+deb11u8".to_string()));
784 assert_eq!(pkg.namespace, Some("debian".to_string()));
785 assert!(pkg.purl.is_some());
786 assert!(
787 pkg.purl
788 .as_ref()
789 .unwrap()
790 .contains("pkg:deb/debian/base-files")
791 );
792 }
793}