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