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