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