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