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