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