1use std::collections::HashMap;
5use std::path::Path;
6
7use crate::models::{
8 DatasourceId, Dependency, FileReference, Md5Digest, PackageData, PackageType, Sha1Digest,
9 Sha256Digest, Sha512Digest,
10};
11use crate::parser_warn as warn;
12use packageurl::PackageUrl;
13use serde_json::Value;
14
15use super::PackageParser;
16use super::license_normalization::normalize_spdx_declared_license;
17use super::utils::{read_file_to_string, truncate_field};
18
19pub struct BitbakeRecipeParser;
20
21impl PackageParser for BitbakeRecipeParser {
22 const PACKAGE_TYPE: PackageType = PackageType::Bitbake;
23
24 fn is_match(path: &Path) -> bool {
25 path.extension()
26 .and_then(|ext| ext.to_str())
27 .is_some_and(|ext| matches!(ext, "bb" | "bbappend"))
28 }
29
30 fn extract_packages(path: &Path) -> Vec<PackageData> {
31 let datasource_id = datasource_id_for_path(path);
32 let content = match read_file_to_string(path, None) {
33 Ok(content) => content,
34 Err(error) => {
35 warn!("Failed to read BitBake recipe at {:?}: {}", path, error);
36 return vec![default_package_data(datasource_id)];
37 }
38 };
39
40 vec![parse_recipe(&content, path, datasource_id)]
41 }
42}
43
44fn datasource_id_for_path(path: &Path) -> DatasourceId {
45 match path.extension().and_then(|ext| ext.to_str()) {
46 Some("bbappend") => DatasourceId::BitbakeRecipeAppend,
47 _ => DatasourceId::BitbakeRecipe,
48 }
49}
50
51fn parse_recipe(content: &str, path: &Path, datasource_id: DatasourceId) -> PackageData {
52 let vars = extract_variables(content);
53 let (filename_name, filename_version) = parse_recipe_filename(path);
54
55 let mut package = default_package_data(datasource_id);
56 let mut extra_data: HashMap<String, Value> = HashMap::new();
57
58 let name = vars
59 .get("PN")
60 .cloned()
61 .or(filename_name)
62 .map(truncate_field);
63 let version = vars
64 .get("PV")
65 .cloned()
66 .or(filename_version)
67 .map(truncate_field);
68
69 package.name = name.clone();
70 package.version = version.clone();
71
72 if let Some(summary) = vars.get("SUMMARY") {
73 package.description = Some(truncate_field(summary.clone()));
74 } else if let Some(description) = vars.get("DESCRIPTION") {
75 package.description = Some(truncate_field(description.clone()));
76 }
77
78 if let Some(homepage) = vars.get("HOMEPAGE") {
79 package.homepage_url = Some(truncate_field(homepage.clone()));
80 }
81
82 if let Some(bugtracker) = vars.get("BUGTRACKER") {
83 package.bug_tracking_url = Some(truncate_field(bugtracker.clone()));
84 }
85
86 if let Some(license) = select_license_value(&vars, name.as_deref()) {
87 package.extracted_license_statement = Some(truncate_field(license.clone()));
88
89 let normalized = normalize_bitbake_license(&license);
90 let (declared, spdx, detections) =
91 normalize_spdx_declared_license(Some(normalized.as_str()));
92 package.declared_license_expression = declared;
93 package.declared_license_expression_spdx = spdx;
94 package.license_detections = detections;
95 }
96
97 if let Some(section) = vars.get("SECTION") {
98 extra_data.insert("section".to_string(), Value::String(section.clone()));
99 }
100
101 let mut file_references = Vec::new();
102 if let Some(lic_files) = vars.get("LIC_FILES_CHKSUM") {
103 merge_file_references(
104 &mut file_references,
105 extract_lic_files_chksum_references(lic_files),
106 );
107 }
108
109 if let Some(src_uri) = vars.get("SRC_URI") {
110 let (remote_entries, local_references) = extract_src_uri_data(src_uri);
111 let uris: Vec<String> = remote_entries
112 .iter()
113 .map(|entry| entry.uri.clone())
114 .collect();
115 if !uris.is_empty() {
116 extra_data.insert(
117 "src_uri".to_string(),
118 Value::Array(uris.into_iter().map(Value::String).collect()),
119 );
120 }
121 merge_file_references(&mut file_references, local_references);
122 apply_src_uri_package_metadata(&mut package, &vars, &remote_entries);
123 }
124
125 let inherits = extract_inherits(content);
126 if !inherits.is_empty() {
127 extra_data.insert(
128 "inherit".to_string(),
129 Value::Array(inherits.into_iter().map(Value::String).collect()),
130 );
131 }
132
133 let mut dependencies = Vec::new();
134
135 if let Some(depends) = vars.get("DEPENDS") {
136 dependencies.extend(
137 parse_dependency_list(depends)
138 .into_iter()
139 .map(|dependency| Dependency {
140 purl: build_dependency_purl(&dependency.name),
141 extracted_requirement: dependency.requirement,
142 scope: Some("build".to_string()),
143 is_runtime: Some(false),
144 is_optional: None,
145 is_pinned: None,
146 is_direct: Some(true),
147 resolved_package: None,
148 extra_data: None,
149 }),
150 );
151 }
152
153 for (key, value) in &vars {
154 if is_rdepends_key(key) {
155 dependencies.extend(parse_dependency_list(value).into_iter().map(|dependency| {
156 Dependency {
157 purl: build_dependency_purl(&dependency.name),
158 extracted_requirement: dependency.requirement,
159 scope: Some("runtime".to_string()),
160 is_runtime: Some(true),
161 is_optional: None,
162 is_pinned: None,
163 is_direct: Some(true),
164 resolved_package: None,
165 extra_data: None,
166 }
167 }));
168 }
169 }
170
171 package.dependencies = dependencies;
172 package.file_references = file_references;
173 package.extra_data = (!extra_data.is_empty()).then_some(extra_data);
174 package.purl = name
175 .as_deref()
176 .and_then(|n| build_package_purl(n, version.as_deref()));
177
178 package
179}
180
181fn default_package_data(datasource_id: DatasourceId) -> PackageData {
182 PackageData {
183 package_type: Some(PackageType::Bitbake),
184 datasource_id: Some(datasource_id),
185 ..Default::default()
186 }
187}
188
189fn parse_recipe_filename(path: &Path) -> (Option<String>, Option<String>) {
190 let stem = match path.file_stem().and_then(|s| s.to_str()) {
191 Some(s) => s,
192 None => return (None, None),
193 };
194
195 match stem.split_once('_') {
196 Some((name, version)) if !name.is_empty() && !version.is_empty() => {
197 let version = (!version.contains('%')).then_some(version.to_string());
198 (Some(name.to_string()), version)
199 }
200 _ => {
201 let trimmed_stem = stem.trim_end_matches('%');
202 let name = if trimmed_stem.is_empty() {
203 stem.to_string()
204 } else {
205 trimmed_stem.to_string()
206 };
207 (Some(name), None)
208 }
209 }
210}
211
212fn select_license_value(
213 vars: &HashMap<String, String>,
214 package_name: Option<&str>,
215) -> Option<String> {
216 let mut candidate_keys = Vec::new();
217
218 if let Some(package_name) = package_name {
219 candidate_keys.push(format!("LICENSE:{package_name}"));
220 candidate_keys.push(format!("LICENSE_{package_name}"));
221 }
222
223 candidate_keys.extend([
224 "LICENSE:${PN}".to_string(),
225 "LICENSE_${PN}".to_string(),
226 "LICENSE".to_string(),
227 ]);
228
229 candidate_keys
230 .into_iter()
231 .find_map(|candidate| vars.get(&candidate).cloned())
232}
233
234fn apply_src_uri_package_metadata(
235 package: &mut PackageData,
236 vars: &HashMap<String, String>,
237 remote_entries: &[SrcUriEntry],
238) {
239 if remote_entries.len() != 1 {
240 return;
241 }
242
243 let entry = &remote_entries[0];
244 package.download_url = Some(entry.uri.clone());
245 package.sha1 = parse_sha1_digest(
246 entry
247 .sha1sum
248 .as_deref()
249 .or_else(|| src_uri_varflag_value(vars, entry.name.as_deref(), "sha1sum")),
250 );
251 package.md5 = parse_md5_digest(
252 entry
253 .md5sum
254 .as_deref()
255 .or_else(|| src_uri_varflag_value(vars, entry.name.as_deref(), "md5sum")),
256 );
257 package.sha256 = parse_sha256_digest(
258 entry
259 .sha256sum
260 .as_deref()
261 .or_else(|| src_uri_varflag_value(vars, entry.name.as_deref(), "sha256sum")),
262 );
263 package.sha512 = parse_sha512_digest(
264 entry
265 .sha512sum
266 .as_deref()
267 .or_else(|| src_uri_varflag_value(vars, entry.name.as_deref(), "sha512sum")),
268 );
269}
270
271fn src_uri_varflag_value<'a>(
272 vars: &'a HashMap<String, String>,
273 name: Option<&str>,
274 algorithm: &str,
275) -> Option<&'a str> {
276 name.and_then(|name| vars.get(&format!("SRC_URI[{name}.{algorithm}]")))
277 .or_else(|| vars.get(&format!("SRC_URI[{algorithm}]")))
278 .map(String::as_str)
279}
280
281fn parse_sha1_digest(value: Option<&str>) -> Option<Sha1Digest> {
282 value.and_then(|value| Sha1Digest::from_hex(value).ok())
283}
284
285fn parse_md5_digest(value: Option<&str>) -> Option<Md5Digest> {
286 value.and_then(|value| Md5Digest::from_hex(value).ok())
287}
288
289fn parse_sha256_digest(value: Option<&str>) -> Option<Sha256Digest> {
290 value.and_then(|value| Sha256Digest::from_hex(value).ok())
291}
292
293fn parse_sha512_digest(value: Option<&str>) -> Option<Sha512Digest> {
294 value.and_then(|value| Sha512Digest::from_hex(value).ok())
295}
296
297#[derive(Default)]
298struct OverrideMutations {
299 appends: Vec<String>,
300 prepends: Vec<String>,
301 removes: Vec<String>,
302}
303
304fn extract_variables(content: &str) -> HashMap<String, String> {
305 let mut vars: HashMap<String, String> = HashMap::new();
306 let mut override_mutations: HashMap<String, OverrideMutations> = HashMap::new();
307 let mut lines = content.lines().peekable();
308
309 while let Some(line) = lines.next() {
310 let trimmed = line.trim();
311
312 if trimmed.is_empty() || trimmed.starts_with('#') {
313 continue;
314 }
315
316 let mut full_line = trimmed.to_string();
317 while full_line.ends_with('\\') {
318 full_line.truncate(full_line.len() - 1);
319 if let Some(next) = lines.next() {
320 full_line.push(' ');
321 full_line.push_str(next.trim());
322 } else {
323 break;
324 }
325 }
326
327 if let Some((var_name, value, op)) = parse_assignment(&full_line) {
328 let cleaned = strip_quotes(&value);
329 match op {
330 AssignOp::Set | AssignOp::Immediate => {
331 vars.insert(var_name, cleaned);
332 }
333 AssignOp::WeakSet | AssignOp::WeakDefault => {
334 vars.entry(var_name).or_insert(cleaned);
335 }
336 AssignOp::Append => {
337 vars.entry(var_name.clone())
338 .and_modify(|v| {
339 v.push(' ');
340 v.push_str(&cleaned);
341 })
342 .or_insert(cleaned);
343 }
344 AssignOp::Prepend => {
345 vars.entry(var_name.clone())
346 .and_modify(|v| {
347 let mut new = cleaned.clone();
348 new.push(' ');
349 new.push_str(v);
350 *v = new;
351 })
352 .or_insert(cleaned);
353 }
354 AssignOp::AppendNoSpace => {
355 vars.entry(var_name.clone())
356 .and_modify(|v| v.push_str(&cleaned))
357 .or_insert(cleaned);
358 }
359 AssignOp::PrependNoSpace => {
360 vars.entry(var_name.clone())
361 .and_modify(|v| {
362 let mut new = cleaned.clone();
363 new.push_str(v);
364 *v = new;
365 })
366 .or_insert(cleaned);
367 }
368 AssignOp::OverrideAppend => {
369 override_mutations
370 .entry(var_name)
371 .or_default()
372 .appends
373 .push(cleaned);
374 }
375 AssignOp::OverridePrepend => {
376 override_mutations
377 .entry(var_name)
378 .or_default()
379 .prepends
380 .push(cleaned);
381 }
382 AssignOp::OverrideRemove => {
383 override_mutations
384 .entry(var_name)
385 .or_default()
386 .removes
387 .push(cleaned);
388 }
389 }
390 }
391 }
392
393 apply_override_mutations(&mut vars, override_mutations);
394
395 vars
396}
397
398fn apply_override_mutations(
399 vars: &mut HashMap<String, String>,
400 override_mutations: HashMap<String, OverrideMutations>,
401) {
402 for (var_name, mutations) in override_mutations {
403 let value = vars.entry(var_name).or_default();
404
405 for append in mutations.appends {
406 value.push_str(&append);
407 }
408
409 if !mutations.prepends.is_empty() {
410 let mut prefix = String::new();
411 for prepend in mutations.prepends {
412 prefix.push_str(&prepend);
413 }
414 value.insert_str(0, &prefix);
415 }
416
417 for remove in mutations.removes {
418 *value = remove_override_tokens(value, &remove);
419 }
420 }
421}
422
423fn remove_override_tokens(current: &str, remove: &str) -> String {
424 let removal_tokens: Vec<&str> = remove.split_whitespace().collect();
425 if removal_tokens.is_empty() {
426 return current.to_string();
427 }
428
429 current
430 .split_whitespace()
431 .filter(|token| !removal_tokens.contains(token))
432 .collect::<Vec<_>>()
433 .join(" ")
434}
435
436#[derive(Debug, Clone, Copy, PartialEq)]
437enum AssignOp {
438 Set,
439 Immediate,
440 WeakSet,
441 WeakDefault,
442 Append,
443 Prepend,
444 AppendNoSpace,
445 PrependNoSpace,
446 OverrideAppend,
447 OverridePrepend,
448 OverrideRemove,
449}
450
451fn parse_assignment(line: &str) -> Option<(String, String, AssignOp)> {
452 let operators: &[(&str, AssignOp)] = &[
453 ("??=", AssignOp::WeakDefault),
454 ("?=", AssignOp::WeakSet),
455 (":=", AssignOp::Immediate),
456 ("+=", AssignOp::Append),
457 ("=+", AssignOp::Prepend),
458 (".=", AssignOp::AppendNoSpace),
459 ("=.", AssignOp::PrependNoSpace),
460 ("=", AssignOp::Set),
461 ];
462
463 for (op_str, op) in operators {
464 if let Some(pos) = line.find(op_str) {
465 let raw_var_name = line[..pos].trim();
466 if raw_var_name.is_empty() || !is_valid_var_name(raw_var_name) {
467 continue;
468 }
469
470 let (var_name, op) = parse_override_var_name(raw_var_name)
471 .unwrap_or_else(|| (raw_var_name.to_string(), *op));
472 let value = line[pos + op_str.len()..].trim().to_string();
473
474 return Some((var_name, value, op));
475 }
476 }
477
478 None
479}
480
481fn parse_override_var_name(var_name: &str) -> Option<(String, AssignOp)> {
482 let colon_segments: Vec<&str> = var_name.split(':').collect();
483 if colon_segments.len() > 1 {
484 for (index, segment) in colon_segments.iter().enumerate() {
485 let op = match *segment {
486 "append" => AssignOp::OverrideAppend,
487 "prepend" => AssignOp::OverridePrepend,
488 "remove" => AssignOp::OverrideRemove,
489 _ => continue,
490 };
491
492 let canonical = colon_segments
493 .iter()
494 .enumerate()
495 .filter_map(|(current, segment)| (current != index).then_some(*segment))
496 .collect::<Vec<_>>()
497 .join(":");
498
499 return Some((canonical, op));
500 }
501 }
502
503 for (suffix, op) in [
504 ("_append", AssignOp::OverrideAppend),
505 ("_prepend", AssignOp::OverridePrepend),
506 ("_remove", AssignOp::OverrideRemove),
507 ] {
508 if let Some(base) = var_name.strip_suffix(suffix) {
509 return Some((base.to_string(), op));
510 }
511 }
512
513 None
514}
515
516fn is_valid_var_name(s: &str) -> bool {
517 let base = s.split([':', '[']).next().unwrap_or(s);
518 !base.is_empty()
519 && base
520 .chars()
521 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$' || c == '{' || c == '}')
522}
523
524fn strip_quotes(s: &str) -> String {
525 let trimmed = s.trim();
526 if trimmed.len() >= 2
527 && ((trimmed.starts_with('"') && trimmed.ends_with('"'))
528 || (trimmed.starts_with('\'') && trimmed.ends_with('\'')))
529 {
530 trimmed[1..trimmed.len() - 1].to_string()
531 } else {
532 trimmed.to_string()
533 }
534}
535
536fn extract_inherits(content: &str) -> Vec<String> {
537 let mut inherits = Vec::new();
538 for line in content.lines() {
539 let trimmed = line.trim();
540 if let Some(rest) = trimmed.strip_prefix("inherit ") {
541 for class in rest.split_whitespace() {
542 if !class.starts_with('#') {
543 inherits.push(class.to_string());
544 } else {
545 break;
546 }
547 }
548 }
549 }
550 inherits
551}
552
553fn is_rdepends_key(key: &str) -> bool {
554 key == "RDEPENDS"
555 || key.starts_with("RDEPENDS:")
556 || key.starts_with("RDEPENDS_")
557 || key.starts_with("RDEPENDS[")
558}
559
560#[derive(Debug, Clone, PartialEq, Eq)]
561struct ParsedDependency {
562 name: String,
563 requirement: Option<String>,
564}
565
566#[derive(Debug, Clone, PartialEq, Eq)]
567struct SrcUriEntry {
568 uri: String,
569 name: Option<String>,
570 sha1sum: Option<String>,
571 md5sum: Option<String>,
572 sha256sum: Option<String>,
573 sha512sum: Option<String>,
574}
575
576fn parse_dependency_list(value: &str) -> Vec<ParsedDependency> {
577 let cleaned_value = strip_bitbake_expansions(value);
578 let tokens: Vec<&str> = cleaned_value.split_whitespace().collect();
579 let mut dependencies = Vec::new();
580 let mut index = 0;
581
582 while index < tokens.len() {
583 let token = tokens[index];
584 let Some(name) = normalize_dependency_name_token(token) else {
585 index += 1;
586 continue;
587 };
588
589 let mut requirement = None;
590
591 if tokens
592 .get(index + 1)
593 .is_some_and(|next| next.starts_with('('))
594 {
595 let mut pieces = Vec::new();
596 index += 1;
597
598 while index < tokens.len() {
599 let piece = tokens[index];
600 pieces.push(piece);
601 if piece.ends_with(')') {
602 break;
603 }
604 index += 1;
605 }
606
607 let joined = pieces.join(" ");
608 let cleaned = joined
609 .trim()
610 .trim_start_matches('(')
611 .trim_end_matches(')')
612 .trim()
613 .to_string();
614 if !cleaned.is_empty() {
615 requirement = Some(cleaned);
616 }
617 }
618
619 dependencies.push(ParsedDependency { name, requirement });
620 index += 1;
621 }
622
623 dependencies
624}
625
626fn strip_bitbake_expansions(value: &str) -> String {
627 let mut result = String::with_capacity(value.len());
628 let chars: Vec<char> = value.chars().collect();
629 let mut index = 0;
630
631 while index < chars.len() {
632 if chars[index] == '$' && chars.get(index + 1) == Some(&'{') {
633 index += 2;
634 let mut depth = 1;
635 while index < chars.len() && depth > 0 {
636 match chars[index] {
637 '{' => depth += 1,
638 '}' => depth -= 1,
639 _ => {}
640 }
641 index += 1;
642 }
643 result.push(' ');
644 continue;
645 }
646
647 result.push(chars[index]);
648 index += 1;
649 }
650
651 result
652}
653
654fn normalize_dependency_name_token(token: &str) -> Option<String> {
655 let trimmed = token.trim_matches(|c| matches!(c, '"' | '\'' | ','));
656 if trimmed.is_empty() || trimmed.contains('$') {
657 return None;
658 }
659
660 let first = trimmed.chars().next()?;
661 if !first.is_ascii_alphanumeric() {
662 return None;
663 }
664
665 if trimmed
666 .chars()
667 .all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '+' | '.' | '/'))
668 {
669 Some(trimmed.to_string())
670 } else {
671 None
672 }
673}
674
675fn extract_src_uri_data(src_uri: &str) -> (Vec<SrcUriEntry>, Vec<FileReference>) {
676 let mut remote_entries = Vec::new();
677 let mut local_references = Vec::new();
678
679 for entry in src_uri.split_whitespace() {
680 if entry.is_empty() {
681 continue;
682 }
683
684 let mut parts = entry.split(';');
685 let base = parts.next().unwrap_or(entry);
686
687 let mut remote_entry = SrcUriEntry {
688 uri: truncate_field(base.to_string()),
689 name: None,
690 sha1sum: None,
691 md5sum: None,
692 sha256sum: None,
693 sha512sum: None,
694 };
695
696 for parameter in parts {
697 let Some((key, value)) = parameter.split_once('=') else {
698 continue;
699 };
700
701 match key {
702 "name" => remote_entry.name = Some(value.to_string()),
703 "sha1sum" => remote_entry.sha1sum = Some(value.to_string()),
704 "md5sum" => remote_entry.md5sum = Some(value.to_string()),
705 "sha256sum" => remote_entry.sha256sum = Some(value.to_string()),
706 "sha512sum" => remote_entry.sha512sum = Some(value.to_string()),
707 _ => {}
708 }
709 }
710
711 if let Some(path) = base.strip_prefix("file://") {
712 if !path.is_empty() {
713 local_references.push(file_reference_from_path(path, "SRC_URI"));
714 }
715 continue;
716 }
717
718 remote_entries.push(remote_entry);
719 }
720
721 (remote_entries, local_references)
722}
723
724fn extract_lic_files_chksum_references(value: &str) -> Vec<FileReference> {
725 let mut references = Vec::new();
726
727 for entry in value.split_whitespace() {
728 let Some(path) = entry
729 .split(';')
730 .next()
731 .and_then(|item| item.strip_prefix("file://"))
732 else {
733 continue;
734 };
735
736 if path.is_empty() {
737 continue;
738 }
739
740 let mut reference = file_reference_from_path(path, "LIC_FILES_CHKSUM");
741 let mut extra_data = reference.extra_data.take().unwrap_or_default();
742
743 for parameter in entry.split(';').skip(1) {
744 let Some((key, raw_value)) = parameter.split_once('=') else {
745 continue;
746 };
747
748 match key {
749 "md5" => {
750 reference.md5 = Md5Digest::from_hex(raw_value).ok();
751 }
752 _ => {
753 extra_data.insert(key.to_string(), Value::String(raw_value.to_string()));
754 }
755 }
756 }
757
758 reference.extra_data = (!extra_data.is_empty()).then_some(extra_data);
759 references.push(reference);
760 }
761
762 references
763}
764
765fn file_reference_from_path(path: &str, source_variable: &str) -> FileReference {
766 let mut reference = FileReference::from_path(truncate_field(path.to_string()));
767 let mut extra_data = HashMap::new();
768 extra_data.insert(
769 "source_variable".to_string(),
770 Value::String(source_variable.to_string()),
771 );
772 reference.extra_data = Some(extra_data);
773 reference
774}
775
776fn merge_file_references(target: &mut Vec<FileReference>, additions: Vec<FileReference>) {
777 for addition in additions {
778 if let Some(existing) = target
779 .iter_mut()
780 .find(|reference| reference.path == addition.path)
781 {
782 if existing.md5.is_none() {
783 existing.md5 = addition.md5;
784 }
785 if existing.sha1.is_none() {
786 existing.sha1 = addition.sha1;
787 }
788 if existing.sha256.is_none() {
789 existing.sha256 = addition.sha256;
790 }
791 if existing.sha512.is_none() {
792 existing.sha512 = addition.sha512;
793 }
794 if existing.extra_data.is_none() {
795 existing.extra_data = addition.extra_data;
796 } else if let (Some(existing_extra), Some(addition_extra)) =
797 (&mut existing.extra_data, addition.extra_data)
798 {
799 existing_extra.extend(addition_extra);
800 }
801 continue;
802 }
803
804 target.push(addition);
805 }
806}
807
808fn normalize_bitbake_license(license: &str) -> String {
809 let mut result = String::with_capacity(license.len());
810 let mut chars = license.chars().peekable();
811 while let Some(ch) = chars.next() {
812 if ch == '&' {
813 let trimmed = result.trim_end();
814 result.truncate(trimmed.len());
815 result.push_str(" AND ");
816 while chars.peek() == Some(&' ') {
817 chars.next();
818 }
819 } else if ch == '|' {
820 let trimmed = result.trim_end();
821 result.truncate(trimmed.len());
822 result.push_str(" OR ");
823 while chars.peek() == Some(&' ') {
824 chars.next();
825 }
826 } else {
827 result.push(ch);
828 }
829 }
830 result
831}
832
833fn build_package_purl(name: &str, version: Option<&str>) -> Option<String> {
834 let mut purl = PackageUrl::new(PackageType::Bitbake.as_str(), name).ok()?;
835 if let Some(v) = version {
836 purl.with_version(v).ok()?;
837 }
838 Some(truncate_field(purl.to_string()))
839}
840
841fn build_dependency_purl(name: &str) -> Option<String> {
842 PackageUrl::new(PackageType::Bitbake.as_str(), name)
843 .ok()
844 .map(|purl| truncate_field(purl.to_string()))
845}
846
847crate::register_parser!(
848 "Yocto BitBake recipe",
849 &["**/*.bb"],
850 "bitbake",
851 "Shell",
852 Some(
853 "https://docs.yoctoproject.org/bitbake/bitbake-user-manual/bitbake-user-manual-metadata.html"
854 ),
855);
856
857crate::register_parser!(
858 "Yocto BitBake append file",
859 &["**/*.bbappend"],
860 "bitbake",
861 "Shell",
862 Some(
863 "https://docs.yoctoproject.org/bitbake/bitbake-user-manual/bitbake-user-manual-metadata.html"
864 ),
865);