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