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