1use std::collections::{HashMap, HashSet, VecDeque};
29use std::path::Path;
30
31use crate::parser_warn as warn;
32use crate::parsers::utils::{
33 MAX_ITERATION_COUNT, RecursionGuard, capped_iteration_limit, read_file_to_string,
34 truncate_field,
35};
36use packageurl::PackageUrl;
37use yaml_serde::{Mapping, Value};
38
39use crate::models::{
40 DatasourceId, Dependency, PackageData, PackageType, ResolvedPackage, Sha256Digest,
41};
42
43use super::PackageParser;
44use super::metadata::ParserMetadata;
45
46const FIELD_NAME: &str = "name";
47const FIELD_VERSION: &str = "version";
48const FIELD_DESCRIPTION: &str = "description";
49const FIELD_HOMEPAGE: &str = "homepage";
50const FIELD_LICENSE: &str = "license";
51const FIELD_REPOSITORY: &str = "repository";
52const FIELD_AUTHOR: &str = "author";
53const FIELD_AUTHORS: &str = "authors";
54const FIELD_DEPENDENCIES: &str = "dependencies";
55const FIELD_DEV_DEPENDENCIES: &str = "dev_dependencies";
56const FIELD_DEPENDENCY_OVERRIDES: &str = "dependency_overrides";
57const FIELD_ENVIRONMENT: &str = "environment";
58const FIELD_ISSUE_TRACKER: &str = "issue_tracker";
59const FIELD_DOCUMENTATION: &str = "documentation";
60const FIELD_EXECUTABLES: &str = "executables";
61const FIELD_PUBLISH_TO: &str = "publish_to";
62const FIELD_ARCHIVE_URL: &str = "archive_url";
63const FIELD_PLATFORMS: &str = "platforms";
64const FIELD_FUNDING: &str = "funding";
65const FIELD_FALSE_SECRETS: &str = "false_secrets";
66const FIELD_SCREENSHOTS: &str = "screenshots";
67const FIELD_TOPICS: &str = "topics";
68const FIELD_IGNORED_ADVISORIES: &str = "ignored_advisories";
69const FIELD_PACKAGES: &str = "packages";
70const FIELD_SDKS: &str = "sdks";
71const FIELD_SDK: &str = "sdk";
72const FIELD_DEPENDENCY: &str = "dependency";
73const FIELD_SHA256: &str = "sha256";
74
75pub struct PubspecYamlParser;
77
78impl PackageParser for PubspecYamlParser {
79 const PACKAGE_TYPE: PackageType = PackageType::Pub;
83
84 fn metadata() -> Vec<ParserMetadata> {
85 vec![ParserMetadata {
86 description: "Dart pubspec.yaml manifest",
87 file_patterns: &["**/pubspec.yaml", "**/pubspec.lock"],
88 package_type: "pub",
89 primary_language: "Dart",
90 documentation_url: Some("https://dart.dev/tools/pub/pubspec"),
91 }]
92 }
93
94 fn extract_packages(path: &Path) -> Vec<PackageData> {
95 let yaml_content = match read_yaml_file(path) {
96 Ok(content) => content,
97 Err(e) => {
98 warn!("Failed to read pubspec.yaml at {:?}: {}", path, e);
99 let mut package_data = default_package_data();
100 package_data.datasource_id = Some(DatasourceId::PubspecYaml);
101 return vec![package_data];
102 }
103 };
104
105 vec![parse_pubspec_yaml(&yaml_content)]
106 }
107
108 fn is_match(path: &Path) -> bool {
109 path.file_name()
110 .and_then(|name| name.to_str())
111 .is_some_and(|name| {
112 name == "pubspec.yaml"
113 || name.ends_with("-pubspec.yaml")
114 || name.ends_with(".pubspec.yaml")
115 })
116 }
117}
118
119pub struct PubspecLockParser;
121
122impl PackageParser for PubspecLockParser {
123 const PACKAGE_TYPE: PackageType = PackageType::Pub;
124
125 fn extract_packages(path: &Path) -> Vec<PackageData> {
126 let yaml_content = match read_yaml_file(path) {
127 Ok(content) => content,
128 Err(e) => {
129 warn!("Failed to read pubspec.lock at {:?}: {}", path, e);
130 let mut package_data =
131 default_package_data_with_type(PubspecLockParser::PACKAGE_TYPE.as_str());
132 package_data.datasource_id = Some(DatasourceId::PubspecLock);
133 return vec![package_data];
134 }
135 };
136
137 vec![parse_pubspec_lock(&yaml_content)]
138 }
139
140 fn is_match(path: &Path) -> bool {
141 path.file_name()
142 .and_then(|name| name.to_str())
143 .is_some_and(|name| {
144 name == "pubspec.lock"
145 || name.ends_with("-pubspec.lock")
146 || name.ends_with(".pubspec.lock")
147 })
148 }
149}
150
151fn read_yaml_file(path: &Path) -> Result<Value, String> {
152 let content =
153 read_file_to_string(path, None).map_err(|e| format!("Failed to read file: {}", e))?;
154 yaml_serde::from_str(&content).map_err(|e| format!("Failed to parse YAML: {}", e))
155}
156
157fn parse_pubspec_yaml(yaml_content: &Value) -> PackageData {
158 let name = extract_string_field(yaml_content, FIELD_NAME).map(truncate_field);
159 let version = extract_string_field(yaml_content, FIELD_VERSION).map(truncate_field);
160 let description = extract_description_field(yaml_content).map(truncate_field);
161 let homepage_url = extract_string_field(yaml_content, FIELD_HOMEPAGE).map(truncate_field);
162 let raw_license = extract_string_field(yaml_content, FIELD_LICENSE).map(truncate_field);
163 let vcs_url = extract_string_field(yaml_content, FIELD_REPOSITORY).map(truncate_field);
164 let bug_tracking_url =
165 extract_string_field(yaml_content, FIELD_ISSUE_TRACKER).map(truncate_field);
166 let archive_url = extract_string_field(yaml_content, FIELD_ARCHIVE_URL).map(truncate_field);
167
168 let parties = extract_authors(yaml_content);
169
170 let declared_license_expression = None;
171 let declared_license_expression_spdx = None;
172 let license_detections = Vec::new();
173
174 let dependencies = [
175 collect_dependencies(
176 yaml_content,
177 FIELD_DEPENDENCIES,
178 Some("dependencies"),
179 true,
180 false,
181 ),
182 collect_dependencies(
183 yaml_content,
184 FIELD_DEV_DEPENDENCIES,
185 Some("dev_dependencies"),
186 false,
187 true,
188 ),
189 collect_dependencies(
190 yaml_content,
191 FIELD_DEPENDENCY_OVERRIDES,
192 Some("dependency_overrides"),
193 true,
194 false,
195 ),
196 collect_dependencies(
197 yaml_content,
198 FIELD_ENVIRONMENT,
199 Some("environment"),
200 true,
201 false,
202 ),
203 ]
204 .concat();
205
206 let extra_data = build_extra_data(yaml_content);
207 let keywords = extract_string_list_field(yaml_content, FIELD_TOPICS);
208
209 let purl = name
210 .as_ref()
211 .and_then(|name| build_purl(name, version.as_deref()))
212 .map(truncate_field);
213
214 let (api_data_url, repository_homepage_url, repository_download_url) =
215 if let (Some(name_val), Some(version_val)) = (&name, &version) {
216 (
217 Some(truncate_field(format!(
218 "https://pub.dev/api/packages/{}/versions/{}",
219 name_val, version_val
220 ))),
221 Some(truncate_field(format!(
222 "https://pub.dev/packages/{}/versions/{}",
223 name_val, version_val
224 ))),
225 Some(truncate_field(format!(
226 "https://pub.dartlang.org/packages/{}/versions/{}.tar.gz",
227 name_val, version_val
228 ))),
229 )
230 } else {
231 (None, None, None)
232 };
233
234 let download_url = archive_url.or_else(|| repository_download_url.clone());
235
236 PackageData {
237 package_type: Some(PubspecYamlParser::PACKAGE_TYPE),
238 namespace: None,
239 name,
240 version,
241 qualifiers: None,
242 subpath: None,
243 primary_language: Some("dart".to_string()),
244 description,
245 release_date: None,
246 parties,
247 keywords,
248 homepage_url,
249 download_url,
250 size: None,
251 sha1: None,
252 md5: None,
253 sha256: None,
254 sha512: None,
255 bug_tracking_url,
256 code_view_url: None,
257 vcs_url,
258 copyright: None,
259 holder: None,
260 declared_license_expression,
261 declared_license_expression_spdx,
262 license_detections,
263 other_license_expression: None,
264 other_license_expression_spdx: None,
265 other_license_detections: Vec::new(),
266 extracted_license_statement: raw_license,
267 notice_text: None,
268 source_packages: Vec::new(),
269 file_references: Vec::new(),
270 is_private: yaml_content
271 .get(FIELD_PUBLISH_TO)
272 .and_then(Value::as_str)
273 .is_some_and(|value| value.trim() == "none"),
274 is_virtual: false,
275 extra_data,
276 dependencies,
277 repository_homepage_url,
278 repository_download_url,
279 api_data_url,
280 datasource_id: Some(DatasourceId::PubspecYaml),
281 purl,
282 }
283}
284
285fn parse_pubspec_lock(yaml_content: &Value) -> PackageData {
286 let dependencies = extract_lock_dependencies(yaml_content);
287
288 let mut package_data = default_package_data_with_type(PubspecLockParser::PACKAGE_TYPE.as_str());
289 package_data.dependencies = dependencies;
290 package_data.datasource_id = Some(DatasourceId::PubspecLock);
291 package_data
292}
293
294fn extract_lock_dependencies(lock_data: &Value) -> Vec<Dependency> {
295 let mut dependencies = Vec::new();
296
297 if let Some(sdks) = lock_data.get(FIELD_SDKS).and_then(Value::as_mapping) {
298 let sdks_limit = capped_iteration_limit(sdks.len(), "pubspec.lock SDKs");
299 for (name_value, version_value) in sdks.iter().take(sdks_limit) {
300 if let (Some(name), Some(version_str)) = (name_value.as_str(), version_value.as_str()) {
301 let purl = build_dependency_purl(name, None).map(truncate_field);
302 dependencies.push(Dependency {
303 purl,
304 extracted_requirement: Some(truncate_field(version_str.to_string())),
305 scope: Some("sdk".to_string()),
306 is_runtime: Some(true),
307 is_optional: Some(false),
308 is_pinned: Some(false),
309 is_direct: Some(true),
310 resolved_package: None,
311 extra_data: None,
312 });
313 }
314 }
315 } else if let Some(version_str) = lock_data.get(FIELD_SDK).and_then(Value::as_str) {
316 let purl = build_dependency_purl("dart", None).map(truncate_field);
317 dependencies.push(Dependency {
318 purl,
319 extracted_requirement: Some(truncate_field(version_str.to_string())),
320 scope: Some("sdk".to_string()),
321 is_runtime: Some(true),
322 is_optional: Some(false),
323 is_pinned: Some(false),
324 is_direct: Some(true),
325 resolved_package: None,
326 extra_data: None,
327 });
328 }
329
330 let Some(packages) = lock_data.get(FIELD_PACKAGES).and_then(Value::as_mapping) else {
331 return dependencies;
332 };
333
334 let runtime_reachable =
335 reachable_lock_packages(packages, &["direct main", "direct overridden"]);
336 let dev_only_reachable = reachable_lock_packages(packages, &["direct dev"]);
337
338 let packages_limit = capped_iteration_limit(packages.len(), "pubspec.lock packages");
339 for (name_value, details_value) in packages.iter().take(packages_limit) {
340 let name = match name_value.as_str() {
341 Some(value) => value,
342 None => continue,
343 };
344 let Some(details) = details_value.as_mapping() else {
345 continue;
346 };
347
348 let version = mapping_get(details, FIELD_VERSION)
349 .and_then(Value::as_str)
350 .map(|value| truncate_field(value.to_string()));
351 let dependency_kind = mapping_get(details, FIELD_DEPENDENCY)
352 .and_then(Value::as_str)
353 .map(|value| truncate_field(value.to_string()));
354 let (is_runtime, is_optional, is_direct) = classify_lock_dependency(
355 name,
356 dependency_kind.as_deref(),
357 &runtime_reachable,
358 &dev_only_reachable,
359 );
360
361 let is_pinned = version
362 .as_ref()
363 .is_some_and(|value| !value.trim().is_empty());
364
365 let purl = build_dependency_purl(name, version.as_deref()).map(truncate_field);
366 let sha256 = extract_sha256(details).and_then(|h| Sha256Digest::from_hex(&h).ok());
367 let resolved_dependencies = extract_lock_package_dependencies(details);
368 let resolved_package = build_resolved_package(
369 name,
370 &version,
371 sha256,
372 extract_lock_descriptor_extra_data(details),
373 resolved_dependencies,
374 );
375
376 dependencies.push(Dependency {
377 purl,
378 extracted_requirement: version.clone().map(truncate_field),
379 scope: dependency_kind,
380 is_runtime: Some(is_runtime),
381 is_optional: Some(is_optional),
382 is_pinned: Some(is_pinned),
383 is_direct: Some(is_direct),
384 resolved_package: Some(Box::new(resolved_package)),
385 extra_data: extract_lock_descriptor_extra_data(details),
386 });
387 }
388
389 dependencies
390}
391
392fn extract_lock_package_dependencies(details: &Mapping) -> Vec<Dependency> {
393 let mut dependencies = Vec::new();
394
395 let Some(dep_map) = mapping_get(details, FIELD_DEPENDENCIES).and_then(Value::as_mapping) else {
396 return dependencies;
397 };
398
399 let dep_limit = capped_iteration_limit(dep_map.len(), "pubspec.lock package dependencies");
400 for (name_value, requirement_value) in dep_map.iter().take(dep_limit) {
401 let name = match name_value.as_str() {
402 Some(value) => value,
403 None => continue,
404 };
405
406 let requirement = match dependency_requirement_from_value(requirement_value) {
407 Some(value) => value,
408 None => continue,
409 };
410 let is_pinned = is_pubspec_version_pinned(&requirement);
411 let purl = if is_pinned {
412 build_dependency_purl(name, Some(requirement.as_str()))
413 } else {
414 build_dependency_purl(name, None)
415 };
416
417 dependencies.push(Dependency {
418 purl: purl.map(truncate_field),
419 extracted_requirement: Some(truncate_field(requirement)),
420 scope: Some(FIELD_DEPENDENCIES.to_string()),
421 is_runtime: Some(true),
422 is_optional: Some(false),
423 is_pinned: Some(is_pinned),
424 is_direct: Some(false),
425 resolved_package: None,
426 extra_data: None,
427 });
428 }
429
430 dependencies
431}
432
433fn extract_sha256(details: &Mapping) -> Option<String> {
434 let direct = mapping_get(details, FIELD_SHA256)
435 .and_then(Value::as_str)
436 .map(|value| value.to_string());
437
438 if direct.is_some() {
439 return direct;
440 }
441
442 mapping_get(details, FIELD_DESCRIPTION)
443 .and_then(Value::as_mapping)
444 .and_then(|desc_map| mapping_get(desc_map, FIELD_SHA256))
445 .and_then(Value::as_str)
446 .map(|value| value.to_string())
447}
448
449fn build_resolved_package(
450 name: &str,
451 version: &Option<String>,
452 sha256: Option<Sha256Digest>,
453 extra_data: Option<HashMap<String, serde_json::Value>>,
454 dependencies: Vec<Dependency>,
455) -> ResolvedPackage {
456 ResolvedPackage {
457 primary_language: Some("dart".to_string()),
458 download_url: None,
459 sha1: None,
460 sha256,
461 sha512: None,
462 md5: None,
463 is_virtual: true,
464 extra_data,
465 dependencies,
466 repository_homepage_url: None,
467 repository_download_url: None,
468 api_data_url: None,
469 datasource_id: None,
470 purl: None,
471 ..ResolvedPackage::new(
472 PubspecLockParser::PACKAGE_TYPE,
473 String::new(),
474 truncate_field(name.to_string()),
475 truncate_field(version.clone().unwrap_or_default()),
476 )
477 }
478}
479
480fn collect_dependencies(
481 yaml_content: &Value,
482 field: &str,
483 scope: Option<&str>,
484 is_runtime: bool,
485 is_optional: bool,
486) -> Vec<Dependency> {
487 let mut dependencies = Vec::new();
488
489 let Some(dep_map) = yaml_content.get(field).and_then(Value::as_mapping) else {
490 return dependencies;
491 };
492
493 let dep_limit = capped_iteration_limit(dep_map.len(), "pubspec.yaml dependencies");
494 for (name_value, requirement_value) in dep_map.iter().take(dep_limit) {
495 let name = match name_value.as_str() {
496 Some(value) => value,
497 None => continue,
498 };
499 let requirement = dependency_requirement_from_value(requirement_value).map(truncate_field);
500 let is_pinned = requirement
501 .as_deref()
502 .is_some_and(is_pubspec_version_pinned);
503 let purl = if is_pinned {
504 build_dependency_purl(name, requirement.as_deref())
505 } else {
506 build_dependency_purl(name, None)
507 };
508
509 dependencies.push(Dependency {
510 purl: purl.map(truncate_field),
511 extracted_requirement: requirement,
512 scope: scope.map(|value| value.to_string()),
513 is_runtime: Some(is_runtime),
514 is_optional: Some(is_optional),
515 is_pinned: Some(is_pinned),
516 is_direct: Some(true),
517 resolved_package: None,
518 extra_data: extract_manifest_dependency_extra_data(requirement_value),
519 });
520 }
521
522 dependencies
523}
524
525fn dependency_requirement_from_value(value: &Value) -> Option<String> {
526 if let Some(value) = value.as_str() {
527 let trimmed = value.trim();
528 if trimmed.is_empty() {
529 return None;
530 }
531 return Some(trimmed.to_string());
532 }
533
534 if let Some(value) = value.as_i64() {
535 return Some(value.to_string());
536 }
537
538 if let Some(value) = value.as_f64() {
539 return Some(value.to_string());
540 }
541
542 if let Some(map) = value.as_mapping() {
543 return format_dependency_mapping(map, &mut RecursionGuard::depth_only());
544 }
545
546 None
547}
548
549fn format_dependency_mapping(map: &Mapping, guard: &mut RecursionGuard<()>) -> Option<String> {
550 if guard.descend() {
551 warn!("Recursion depth exceeded in format_dependency_mapping");
552 return None;
553 }
554
555 let mut parts = Vec::new();
556
557 let map_limit = capped_iteration_limit(map.len(), "pubspec dependency mapping");
558 for (key, value) in map.iter().take(map_limit) {
559 let Some(key_str) = key.as_str() else {
560 continue;
561 };
562
563 let value_str = if let Some(value) = value.as_str() {
564 value.to_string()
565 } else if let Some(value) = value.as_i64() {
566 value.to_string()
567 } else if let Some(value) = value.as_f64() {
568 value.to_string()
569 } else if let Some(nested) = value.as_mapping() {
570 format_dependency_mapping(nested, guard)?
571 } else {
572 continue;
573 };
574
575 parts.push(format!("{}: {}", key_str, value_str));
576 }
577
578 guard.ascend();
579
580 if parts.is_empty() {
581 None
582 } else {
583 Some(parts.join(", "))
584 }
585}
586
587fn is_pubspec_version_pinned(version: &str) -> bool {
588 let trimmed = version.trim();
589 if trimmed.is_empty() {
590 return false;
591 }
592
593 trimmed
594 .chars()
595 .all(|character| character.is_ascii_digit() || character == '.')
596}
597
598fn build_purl(name: &str, version: Option<&str>) -> Option<String> {
599 build_purl_with_type(PackageType::Pub.as_str(), name, version)
600}
601
602fn build_dependency_purl(name: &str, version: Option<&str>) -> Option<String> {
603 build_purl_with_type(PackageType::Pub.as_str(), name, version)
604}
605
606fn build_purl_with_type(package_type: &str, name: &str, version: Option<&str>) -> Option<String> {
607 let mut package_url = match PackageUrl::new(package_type, name) {
608 Ok(purl) => purl,
609 Err(e) => {
610 warn!(
611 "Failed to create PackageUrl for {} dependency '{}': {}",
612 package_type, name, e
613 );
614 return None;
615 }
616 };
617
618 if let Some(version) = version
619 && let Err(e) = package_url.with_version(version)
620 {
621 warn!(
622 "Failed to set version '{}' for {} dependency '{}': {}",
623 version, package_type, name, e
624 );
625 return None;
626 }
627
628 Some(package_url.to_string())
629}
630
631fn extract_string_field(yaml_content: &Value, field: &str) -> Option<String> {
632 yaml_content
633 .get(field)
634 .and_then(Value::as_str)
635 .map(|value| value.trim().to_string())
636 .filter(|value| !value.is_empty())
637}
638
639fn extract_description_field(yaml_content: &Value) -> Option<String> {
640 yaml_content
643 .get(FIELD_DESCRIPTION)
644 .and_then(Value::as_str)
645 .and_then(|value| {
646 let trimmed = value.trim_start();
648 if trimmed.is_empty() {
649 None
650 } else {
651 Some(trimmed.to_string())
652 }
653 })
654}
655
656fn mapping_get<'a>(map: &'a Mapping, key: &str) -> Option<&'a Value> {
657 map.get(Value::String(key.to_string()))
658}
659
660fn default_package_data() -> PackageData {
661 default_package_data_with_type(PubspecYamlParser::PACKAGE_TYPE.as_str())
662}
663
664fn default_package_data_with_type(package_type: &str) -> PackageData {
665 PackageData {
666 package_type: package_type.parse::<PackageType>().ok(),
667 primary_language: Some("dart".to_string()),
668 ..Default::default()
669 }
670}
671
672fn extract_authors(yaml_content: &Value) -> Vec<crate::models::Party> {
673 use crate::models::Party;
674 let mut parties = Vec::new();
675
676 if let Some(author) = extract_string_field(yaml_content, FIELD_AUTHOR).map(truncate_field) {
677 parties.push(Party {
678 r#type: None,
679 role: Some("author".to_string()),
680 name: Some(author),
681 email: None,
682 url: None,
683 organization: None,
684 organization_url: None,
685 timezone: None,
686 });
687 }
688
689 if let Some(authors_value) = yaml_content.get(FIELD_AUTHORS)
690 && let Some(authors_array) = authors_value.as_sequence()
691 {
692 let authors_limit = capped_iteration_limit(authors_array.len(), "pubspec.yaml authors");
693 for author_value in authors_array.iter().take(authors_limit) {
694 if let Some(author_str) = author_value.as_str() {
695 parties.push(Party {
696 r#type: None,
697 role: Some("author".to_string()),
698 name: Some(truncate_field(author_str.to_string())),
699 email: None,
700 url: None,
701 organization: None,
702 organization_url: None,
703 timezone: None,
704 });
705 }
706 }
707 }
708
709 parties
710}
711
712fn build_extra_data(
713 yaml_content: &Value,
714) -> Option<std::collections::HashMap<String, serde_json::Value>> {
715 use std::collections::HashMap;
716 let mut extra_data = HashMap::new();
717
718 if let Some(issue_tracker) = extract_string_field(yaml_content, FIELD_ISSUE_TRACKER) {
719 extra_data.insert(
720 FIELD_ISSUE_TRACKER.to_string(),
721 serde_json::Value::String(issue_tracker),
722 );
723 }
724
725 if let Some(documentation) = extract_string_field(yaml_content, FIELD_DOCUMENTATION) {
726 extra_data.insert(
727 FIELD_DOCUMENTATION.to_string(),
728 serde_json::Value::String(documentation),
729 );
730 }
731
732 if let Some(executables) = yaml_content.get(FIELD_EXECUTABLES) {
733 if let Ok(json_value) = serde_json::to_value(executables) {
735 extra_data.insert(FIELD_EXECUTABLES.to_string(), json_value);
736 }
737 }
738
739 if let Some(publish_to) = extract_string_field(yaml_content, FIELD_PUBLISH_TO) {
740 extra_data.insert(
741 FIELD_PUBLISH_TO.to_string(),
742 serde_json::Value::String(publish_to),
743 );
744 }
745
746 for field in [
747 FIELD_PLATFORMS,
748 FIELD_FUNDING,
749 FIELD_FALSE_SECRETS,
750 FIELD_SCREENSHOTS,
751 FIELD_TOPICS,
752 FIELD_IGNORED_ADVISORIES,
753 ] {
754 if let Some(value) = yaml_content.get(field)
755 && let Ok(json_value) = serde_json::to_value(value)
756 {
757 extra_data.insert(field.to_string(), json_value);
758 }
759 }
760
761 if extra_data.is_empty() {
762 None
763 } else {
764 Some(extra_data)
765 }
766}
767
768fn extract_string_list_field(yaml_content: &Value, field: &str) -> Vec<String> {
769 yaml_content
770 .get(field)
771 .and_then(Value::as_sequence)
772 .into_iter()
773 .flatten()
774 .filter_map(Value::as_str)
775 .map(str::trim)
776 .filter(|value| !value.is_empty())
777 .map(|value| truncate_field(value.to_string()))
778 .collect()
779}
780
781fn extract_manifest_dependency_extra_data(
782 requirement_value: &Value,
783) -> Option<HashMap<String, serde_json::Value>> {
784 requirement_value
785 .as_mapping()
786 .and_then(|map| serde_json::to_value(map).ok())
787 .and_then(|json| json.as_object().cloned())
788 .map(|map| map.into_iter().collect())
789}
790
791fn extract_lock_descriptor_extra_data(
792 details: &Mapping,
793) -> Option<HashMap<String, serde_json::Value>> {
794 let mut extra = HashMap::new();
795
796 if let Some(source) = mapping_get(details, "source").and_then(Value::as_str) {
797 extra.insert(
798 "source".to_string(),
799 serde_json::Value::String(source.to_string()),
800 );
801 }
802
803 if let Some(description) = mapping_get(details, FIELD_DESCRIPTION)
804 && let Ok(json_value) = serde_json::to_value(description)
805 {
806 extra.insert("description".to_string(), json_value);
807 }
808
809 if let Some(kind) = mapping_get(details, FIELD_DEPENDENCY).and_then(Value::as_str) {
810 extra.insert(
811 FIELD_DEPENDENCY.to_string(),
812 serde_json::Value::String(kind.to_string()),
813 );
814 }
815
816 (!extra.is_empty()).then_some(extra)
817}
818
819fn reachable_lock_packages(packages: &Mapping, roots: &[&str]) -> HashSet<String> {
820 let mut reachable = HashSet::new();
821 let mut queue = VecDeque::new();
822 let mut iterations = 0usize;
823
824 let roots_limit = capped_iteration_limit(packages.len(), "pubspec.lock reachability roots");
825 for (name_value, details_value) in packages.iter().take(roots_limit) {
826 let Some(name) = name_value.as_str() else {
827 continue;
828 };
829 let Some(details) = details_value.as_mapping() else {
830 continue;
831 };
832 let kind = mapping_get(details, FIELD_DEPENDENCY).and_then(Value::as_str);
833 if roots.contains(&kind.unwrap_or_default()) {
834 queue.push_back(truncate_field(name.to_string()));
835 }
836 }
837
838 while let Some(current) = queue.pop_front() {
839 if iterations >= MAX_ITERATION_COUNT {
840 warn!("Iteration count exceeded in reachable_lock_packages BFS");
841 break;
842 }
843 iterations += 1;
844
845 if !reachable.insert(current.clone()) {
846 continue;
847 }
848
849 let Some(details_value) = packages.get(Value::String(current.clone())) else {
850 continue;
851 };
852 let Some(details) = details_value.as_mapping() else {
853 continue;
854 };
855 let Some(dep_map) = mapping_get(details, FIELD_DEPENDENCIES).and_then(Value::as_mapping)
856 else {
857 continue;
858 };
859
860 let dep_keys_limit =
861 capped_iteration_limit(dep_map.len(), "pubspec.lock reachability edges");
862 for dep_name in dep_map
863 .keys()
864 .filter_map(Value::as_str)
865 .take(dep_keys_limit)
866 {
867 queue.push_back(truncate_field(dep_name.to_string()));
868 }
869 }
870
871 reachable
872}
873
874fn classify_lock_dependency(
875 name: &str,
876 dependency_kind: Option<&str>,
877 runtime_reachable: &HashSet<String>,
878 dev_only_reachable: &HashSet<String>,
879) -> (bool, bool, bool) {
880 match dependency_kind {
881 Some("direct main") | Some("direct overridden") => (true, false, true),
882 Some("direct dev") => (false, true, true),
883 Some("transitive") => {
884 if runtime_reachable.contains(name) {
885 (true, false, false)
886 } else if dev_only_reachable.contains(name) {
887 (false, true, false)
888 } else {
889 (true, false, false)
890 }
891 }
892 _ => (true, false, true),
893 }
894}
895
896#[cfg(test)]
897mod is_match_tests {
898 use super::{PubspecLockParser, PubspecYamlParser};
899 use crate::parsers::PackageParser;
900 use std::path::PathBuf;
901
902 #[test]
903 fn test_pubspec_yaml_parser_matches_suffixed_filenames() {
904 assert!(PubspecYamlParser::is_match(&PathBuf::from("pubspec.yaml")));
905 assert!(PubspecYamlParser::is_match(&PathBuf::from(
906 "simple-pubspec.yaml"
907 )));
908 assert!(PubspecYamlParser::is_match(&PathBuf::from(
909 "simple.pubspec.yaml"
910 )));
911 assert!(!PubspecYamlParser::is_match(&PathBuf::from("pubspec.yml")));
912 }
913
914 #[test]
915 fn test_pubspec_lock_parser_matches_suffixed_filenames() {
916 assert!(PubspecLockParser::is_match(&PathBuf::from("pubspec.lock")));
917 assert!(PubspecLockParser::is_match(&PathBuf::from(
918 "dart-pubspec.lock"
919 )));
920 assert!(PubspecLockParser::is_match(&PathBuf::from(
921 "dart.pubspec.lock"
922 )));
923 assert!(!PubspecLockParser::is_match(&PathBuf::from(
924 "pubspec.lock.bak"
925 )));
926 }
927}