1use crate::models::{
26 DatasourceId, Dependency, PackageData, PackageType, ResolvedPackage, Sha1Digest, Sha512Digest,
27};
28use crate::parser_warn as warn;
29use crate::parsers::utils::{
30 MAX_ITERATION_COUNT, MAX_RECURSION_DEPTH, RecursionGuard, npm_purl, parse_sri,
31 read_file_to_string, truncate_field,
32};
33use serde_json::Value;
34use std::collections::HashMap;
35use std::path::Path;
36
37use super::PackageParser;
38use super::metadata::ParserMetadata;
39
40const FIELD_LOCKFILE_VERSION: &str = "lockfileVersion";
42const FIELD_NAME: &str = "name";
43const FIELD_VERSION: &str = "version";
44const FIELD_DEPENDENCIES: &str = "dependencies";
45const FIELD_PACKAGES: &str = "packages";
46const FIELD_RESOLVED: &str = "resolved";
47const FIELD_INTEGRITY: &str = "integrity";
48const FIELD_DEV: &str = "dev";
49const FIELD_OPTIONAL: &str = "optional";
50const FIELD_DEV_OPTIONAL: &str = "devOptional";
51const FIELD_LINK: &str = "link";
52
53pub struct NpmLockParser;
58
59impl PackageParser for NpmLockParser {
60 const PACKAGE_TYPE: PackageType = PackageType::Npm;
61
62 fn metadata() -> Vec<ParserMetadata> {
63 vec![ParserMetadata {
64 description: "npm package-lock.json lockfile",
65 file_patterns: &[
66 "**/package-lock.json",
67 "**/.package-lock.json",
68 "**/npm-shrinkwrap.json",
69 ],
70 package_type: "npm",
71 primary_language: "JavaScript",
72 documentation_url: Some(
73 "https://docs.npmjs.com/cli/v8/configuring-npm/package-lock-json",
74 ),
75 }]
76 }
77
78 fn is_match(path: &Path) -> bool {
79 path.file_name()
80 .and_then(|name| name.to_str())
81 .map(|name| {
82 name == "package-lock.json"
83 || name == ".package-lock.json"
84 || name == "npm-shrinkwrap.json"
85 || name == ".npm-shrinkwrap.json"
86 })
87 .unwrap_or(false)
88 }
89
90 fn extract_packages(path: &Path) -> Vec<PackageData> {
91 let content = match read_file_to_string(path, None) {
92 Ok(content) => content,
93 Err(e) => {
94 warn!("Failed to read package-lock.json at {:?}: {}", path, e);
95 return vec![default_package_data()];
96 }
97 };
98
99 let json: Value = match serde_json::from_str(&content) {
100 Ok(json) => json,
101 Err(e) => {
102 warn!("Failed to parse package-lock.json at {:?}: {}", path, e);
103 return vec![default_package_data()];
104 }
105 };
106
107 let lockfile_version = json
108 .get(FIELD_LOCKFILE_VERSION)
109 .and_then(|v| v.as_i64())
110 .unwrap_or(1);
111
112 let root_name = truncate_field(
113 json.get(FIELD_NAME)
114 .and_then(|v| v.as_str())
115 .unwrap_or("")
116 .to_string(),
117 );
118
119 let root_version = truncate_field(
120 json.get(FIELD_VERSION)
121 .and_then(|v| v.as_str())
122 .unwrap_or("")
123 .to_string(),
124 );
125
126 vec![if lockfile_version == 1 {
127 parse_lockfile_v1(&json, root_name, root_version, lockfile_version)
128 } else {
129 parse_lockfile_v2_plus(&json, root_name, root_version, lockfile_version)
130 }]
131 }
132}
133
134fn default_package_data() -> PackageData {
136 PackageData {
137 package_type: Some(NpmLockParser::PACKAGE_TYPE),
138 datasource_id: Some(DatasourceId::NpmPackageLockJson),
139 ..Default::default()
140 }
141}
142
143fn parse_lockfile_v2_plus(
145 json: &Value,
146 root_name: String,
147 root_version: String,
148 lockfile_version: i64,
149) -> PackageData {
150 let packages = match json.get(FIELD_PACKAGES).and_then(|v| v.as_object()) {
151 Some(packages) => packages,
152 None => {
153 warn!("No 'packages' field found in lockfile v2+");
154 return default_package_data();
155 }
156 };
157
158 let (root_name, root_version) = extract_root_package_identity(json, root_name, root_version);
159 let (namespace, name, version, purl) =
160 normalize_root_package_metadata(&root_name, &root_version);
161 let linked_workspace_names = collect_linked_workspace_names(packages);
162
163 let mut root_deps = std::collections::HashSet::new();
165
166 if let Some(root_deps_obj) = json.get(FIELD_DEPENDENCIES).and_then(|v| v.as_object()) {
168 for key in root_deps_obj.keys().take(MAX_ITERATION_COUNT) {
169 root_deps.insert(key.clone());
170 }
171 }
172 if let Some(root_dev_deps_obj) = json.get("devDependencies").and_then(|v| v.as_object()) {
173 for key in root_dev_deps_obj.keys().take(MAX_ITERATION_COUNT) {
174 root_deps.insert(key.clone());
175 }
176 }
177 if let Some(root_package) = packages.get("").and_then(|value| value.as_object()) {
178 collect_root_dependency_names(root_package.get(FIELD_DEPENDENCIES), &mut root_deps);
179 collect_root_dependency_names(root_package.get("devDependencies"), &mut root_deps);
180 collect_root_dependency_names(root_package.get("optionalDependencies"), &mut root_deps);
181 }
182
183 let mut dependencies = Vec::new();
184
185 for (key, value) in packages.iter().take(MAX_ITERATION_COUNT) {
186 if key.is_empty() {
188 continue;
189 }
190
191 let install_name = extract_package_name_from_path(key);
193 if install_name.is_empty() {
194 continue;
195 }
196
197 let package_name = value
198 .get(FIELD_NAME)
199 .and_then(|v| v.as_str())
200 .map(str::trim)
201 .filter(|name| !name.is_empty())
202 .map(str::to_string)
203 .or_else(|| linked_workspace_names.get(key).cloned())
204 .unwrap_or_else(|| install_name.clone());
205
206 let version = value
207 .get(FIELD_VERSION)
208 .and_then(|v| v.as_str())
209 .map(|v| truncate_field(v.to_string()));
210
211 let is_dev = value
212 .get(FIELD_DEV)
213 .and_then(|v| v.as_bool())
214 .unwrap_or(false);
215 let is_optional = value
216 .get(FIELD_OPTIONAL)
217 .and_then(|v| v.as_bool())
218 .unwrap_or(false);
219 let is_dev_optional = value
220 .get(FIELD_DEV_OPTIONAL)
221 .and_then(|v| v.as_bool())
222 .unwrap_or(false);
223
224 let resolved = value
225 .get(FIELD_RESOLVED)
226 .and_then(|v| v.as_str())
227 .map(|v| truncate_field(v.to_string()));
228 let integrity = value.get(FIELD_INTEGRITY).and_then(|v| v.as_str());
229 let from = value.get("from").and_then(|v| v.as_str());
230 let in_bundle = value
231 .get("inBundle")
232 .and_then(|v| v.as_bool())
233 .unwrap_or(false);
234 let is_link = value
235 .get(FIELD_LINK)
236 .and_then(|v| v.as_bool())
237 .unwrap_or(false);
238 let is_direct = root_deps.contains(&install_name) && is_direct_dependency_path(key);
239
240 let dependency = match version {
241 Some(version) => build_npm_dependency(
242 &package_name,
243 version,
244 is_dev,
245 is_dev_optional,
246 is_optional,
247 resolved,
248 integrity,
249 is_direct,
250 from,
251 in_bundle,
252 Vec::new(),
253 ),
254 None if is_link => build_link_dependency(
255 &package_name,
256 is_dev,
257 is_dev_optional,
258 is_optional,
259 resolved,
260 is_direct,
261 ),
262 None => continue,
263 };
264
265 dependencies.push(dependency);
266 }
267
268 let extra_data = Some(HashMap::from([(
269 "lockfileVersion".to_string(),
270 Value::from(lockfile_version),
271 )]));
272
273 PackageData {
274 package_type: Some(NpmLockParser::PACKAGE_TYPE),
275 namespace: namespace.clone(),
276 name,
277 version,
278 qualifiers: None,
279 subpath: None,
280 primary_language: None,
281 description: None,
282 release_date: None,
283 parties: Vec::new(),
284 keywords: Vec::new(),
285 homepage_url: None,
286 download_url: None,
287 size: None,
288 sha1: None,
289 md5: None,
290 sha256: None,
291 sha512: None,
292 bug_tracking_url: None,
293 code_view_url: None,
294 vcs_url: None,
295 copyright: None,
296 holder: None,
297 declared_license_expression: None,
298 declared_license_expression_spdx: None,
299 license_detections: Vec::new(),
300 other_license_expression: None,
301 other_license_expression_spdx: None,
302 other_license_detections: Vec::new(),
303 extracted_license_statement: None,
304 notice_text: None,
305 source_packages: Vec::new(),
306 file_references: Vec::new(),
307 is_private: false,
308 is_virtual: false,
309 extra_data,
310 dependencies,
311 repository_homepage_url: None,
312 repository_download_url: None,
313 api_data_url: None,
314 datasource_id: Some(DatasourceId::NpmPackageLockJson),
315 purl,
316 }
317}
318
319fn collect_linked_workspace_names(
320 packages: &serde_json::Map<String, Value>,
321) -> HashMap<String, String> {
322 let mut linked_names = HashMap::new();
323
324 for (key, value) in packages.iter().take(MAX_ITERATION_COUNT) {
325 let is_link = value
326 .get(FIELD_LINK)
327 .and_then(|v| v.as_bool())
328 .unwrap_or(false);
329 if !is_link {
330 continue;
331 }
332
333 let Some(resolved) = value.get(FIELD_RESOLVED).and_then(|v| v.as_str()) else {
334 continue;
335 };
336
337 let install_name = extract_package_name_from_path(key);
338 if install_name.is_empty() || install_name == key.as_str() {
339 continue;
340 }
341
342 linked_names.insert(resolved.to_string(), install_name);
343 }
344
345 linked_names
346}
347
348fn parse_lockfile_v1(
350 json: &Value,
351 root_name: String,
352 root_version: String,
353 _lockfile_version: i64,
354) -> PackageData {
355 let dependencies_obj = match json.get(FIELD_DEPENDENCIES).and_then(|v| v.as_object()) {
356 Some(deps) => deps,
357 None => {
358 warn!("No 'dependencies' field found in lockfile v1");
359 return default_package_data();
360 }
361 };
362
363 let (namespace, name, version, purl) =
364 normalize_root_package_metadata(&root_name, &root_version);
365
366 let dependencies = parse_dependencies_v1(dependencies_obj);
367
368 PackageData {
369 package_type: Some(NpmLockParser::PACKAGE_TYPE),
370 namespace: namespace.clone(),
371 name,
372 version,
373 qualifiers: None,
374 subpath: None,
375 primary_language: None,
376 description: None,
377 release_date: None,
378 parties: Vec::new(),
379 keywords: Vec::new(),
380 homepage_url: None,
381 download_url: None,
382 size: None,
383 sha1: None,
384 md5: None,
385 sha256: None,
386 sha512: None,
387 bug_tracking_url: None,
388 code_view_url: None,
389 vcs_url: None,
390 copyright: None,
391 holder: None,
392 declared_license_expression: None,
393 declared_license_expression_spdx: None,
394 license_detections: Vec::new(),
395 other_license_expression: None,
396 other_license_expression_spdx: None,
397 other_license_detections: Vec::new(),
398 extracted_license_statement: None,
399 notice_text: None,
400 source_packages: Vec::new(),
401 file_references: Vec::new(),
402 is_private: false,
403 is_virtual: false,
404 extra_data: None,
405 dependencies,
406 repository_homepage_url: None,
407 repository_download_url: None,
408 api_data_url: None,
409 datasource_id: Some(DatasourceId::NpmPackageLockJson),
410 purl,
411 }
412}
413
414fn parse_dependencies_v1(dependencies_obj: &serde_json::Map<String, Value>) -> Vec<Dependency> {
419 let mut guard = RecursionGuard::<()>::depth_only();
420 parse_dependencies_v1_with_depth(dependencies_obj, &mut guard)
421}
422
423fn parse_dependencies_v1_with_depth(
425 dependencies_obj: &serde_json::Map<String, Value>,
426 guard: &mut RecursionGuard<()>,
427) -> Vec<Dependency> {
428 if guard.descend() {
429 warn!(
430 "Max recursion depth {} exceeded in v1 dependency parsing",
431 MAX_RECURSION_DEPTH
432 );
433 return Vec::new();
434 }
435
436 let mut dependencies = Vec::new();
437
438 for (package_name, dep_data) in dependencies_obj.iter().take(MAX_ITERATION_COUNT) {
439 let version = match dep_data.get(FIELD_VERSION).and_then(|v| v.as_str()) {
440 Some(v) => truncate_field(v.to_string()),
441 None => continue,
442 };
443
444 let is_dev = dep_data
445 .get(FIELD_DEV)
446 .and_then(|v| v.as_bool())
447 .unwrap_or(false);
448 let is_optional = dep_data
449 .get(FIELD_OPTIONAL)
450 .and_then(|v| v.as_bool())
451 .unwrap_or(false);
452
453 let resolved = dep_data
454 .get(FIELD_RESOLVED)
455 .and_then(|v| v.as_str())
456 .map(|v| truncate_field(v.to_string()));
457 let integrity = dep_data.get(FIELD_INTEGRITY).and_then(|v| v.as_str());
458 let from = dep_data.get("from").and_then(|v| v.as_str());
459 let in_bundle = dep_data
460 .get("inBundle")
461 .and_then(|v| v.as_bool())
462 .unwrap_or(false);
463
464 let nested_deps = dep_data
465 .get(FIELD_DEPENDENCIES)
466 .and_then(|v| v.as_object())
467 .map(|nested| parse_dependencies_v1_with_depth(nested, guard))
468 .unwrap_or_default();
469
470 let is_direct = guard.depth() == 1;
471
472 let dependency = build_npm_dependency(
473 package_name,
474 version,
475 is_dev,
476 false,
477 is_optional,
478 resolved,
479 integrity,
480 is_direct,
481 from,
482 in_bundle,
483 nested_deps,
484 );
485
486 dependencies.push(dependency);
487 }
488
489 guard.ascend();
490 dependencies
491}
492
493fn extract_namespace_and_name(package_name: &str) -> (String, String) {
496 if package_name.starts_with('@') {
497 let parts: Vec<&str> = package_name.splitn(2, '/').collect();
499 if parts.len() == 2 {
500 (parts[0].to_string(), parts[1].to_string())
501 } else {
502 (String::new(), package_name.to_string())
504 }
505 } else {
506 (String::new(), package_name.to_string())
508 }
509}
510
511fn extract_package_name_from_path(path: &str) -> String {
513 if let Some(pos) = path.rfind("node_modules/") {
515 let after_node_modules = &path[pos + "node_modules/".len()..];
516
517 if after_node_modules.starts_with('@') {
519 if let Some(slash_pos) = after_node_modules.find('/') {
521 let scope_and_package = &after_node_modules[..=slash_pos];
522 let remaining = &after_node_modules[slash_pos + 1..];
524 if let Some(next_slash) = remaining.find('/') {
525 return format!("{}{}", scope_and_package, &remaining[..next_slash]);
527 } else {
528 return after_node_modules.to_string();
530 }
531 }
532 } else {
533 if let Some(slash_pos) = after_node_modules.find('/') {
535 return after_node_modules[..slash_pos].to_string();
536 } else {
537 return after_node_modules.to_string();
538 }
539 }
540 }
541
542 path.to_string()
543}
544
545fn create_purl(namespace: &str, name: &str, version: Option<&str>) -> Option<String> {
546 let full_name = if namespace.is_empty() {
547 name.to_string()
548 } else {
549 format!("{}/{}", namespace, name)
550 };
551 npm_purl(&full_name, version.filter(|value| !value.is_empty()))
552}
553
554fn normalize_root_package_metadata(
555 root_name: &str,
556 root_version: &str,
557) -> (
558 Option<String>,
559 Option<String>,
560 Option<String>,
561 Option<String>,
562) {
563 let (namespace, name) = extract_namespace_and_name(root_name);
564 let normalized_name = non_empty_string(&name);
565 let normalized_namespace = normalized_name.as_ref().map(|_| namespace);
566 let normalized_version = normalized_name
567 .as_ref()
568 .and_then(|_| non_empty_string(root_version));
569 let purl = normalized_name.as_deref().and_then(|name| {
570 create_purl(
571 normalized_namespace.as_deref().unwrap_or(""),
572 name,
573 normalized_version.as_deref(),
574 )
575 });
576
577 (
578 normalized_namespace,
579 normalized_name,
580 normalized_version,
581 purl,
582 )
583}
584
585fn extract_root_package_identity(
586 json: &Value,
587 root_name: String,
588 root_version: String,
589) -> (String, String) {
590 let root_package = json
591 .get(FIELD_PACKAGES)
592 .and_then(|value| value.as_object())
593 .and_then(|packages| packages.get(""))
594 .and_then(|value| value.as_object());
595
596 let name = non_empty_string(&root_name).or_else(|| {
597 root_package
598 .and_then(|package| package.get(FIELD_NAME))
599 .and_then(|value| value.as_str())
600 .map(str::to_string)
601 .filter(|value| !value.trim().is_empty())
602 });
603 let version = non_empty_string(&root_version).or_else(|| {
604 root_package
605 .and_then(|package| package.get(FIELD_VERSION))
606 .and_then(|value| value.as_str())
607 .map(str::to_string)
608 .filter(|value| !value.trim().is_empty())
609 });
610
611 (name.unwrap_or_default(), version.unwrap_or_default())
612}
613
614fn non_empty_string(value: &str) -> Option<String> {
615 let trimmed = value.trim();
616 if trimmed.is_empty() {
617 None
618 } else {
619 Some(trimmed.to_string())
620 }
621}
622
623fn collect_root_dependency_names(
624 value: Option<&Value>,
625 root_deps: &mut std::collections::HashSet<String>,
626) {
627 if let Some(entries) = value.and_then(|value| value.as_object()) {
628 for key in entries.keys().take(MAX_ITERATION_COUNT) {
629 root_deps.insert(key.clone());
630 }
631 }
632}
633
634fn is_direct_dependency_path(package_path: &str) -> bool {
635 let node_modules_count = package_path.matches("node_modules/").count();
636
637 match node_modules_count {
638 0 => true,
639 1 => package_path.starts_with("node_modules/") || package_path.starts_with(".pnpm/"),
640 _ => false,
641 }
642}
643
644fn parse_integrity_field(integrity: Option<&str>) -> (Option<String>, Option<String>) {
647 let integrity = match integrity {
648 Some(i) => i,
649 None => return (None, None),
650 };
651
652 match parse_sri(integrity) {
653 Some((algo, hex_digest)) => match algo.as_str() {
654 "sha1" => (Some(hex_digest), None),
655 "sha512" => (None, Some(hex_digest)),
656 _ => (None, None),
657 },
658 None => (None, None),
659 }
660}
661
662fn parse_resolved_url(url: &str) -> Option<String> {
665 if let Some(hash_pos) = url.rfind('#') {
667 let hash = &url[hash_pos + 1..];
668 if hash.len() == 40 && hash.chars().all(|c| c.is_ascii_hexdigit()) {
670 return Some(hash.to_string());
671 }
672 }
673 None
674}
675
676fn determine_scope(
679 is_dev: bool,
680 is_dev_optional: bool,
681 is_optional: bool,
682) -> (&'static str, bool, bool) {
683 if is_dev || is_dev_optional {
684 ("devDependencies", false, true)
685 } else if is_optional {
686 ("dependencies", true, true)
687 } else {
688 ("dependencies", true, false)
689 }
690}
691
692fn parse_npm_alias_spec(version_spec: &str) -> Option<(String, String, String)> {
693 let aliased_spec = version_spec.strip_prefix("npm:")?;
694 let (aliased_name, constraint) = aliased_spec.rsplit_once('@')?;
695 let (namespace, name) = extract_namespace_and_name(aliased_name);
696
697 if name.is_empty() || constraint.trim().is_empty() {
698 None
699 } else {
700 Some((namespace, name, constraint.to_string()))
701 }
702}
703
704fn is_exact_version(version: &str) -> bool {
705 let version = version.trim();
706
707 if version.is_empty() {
708 return false;
709 }
710
711 if version.starts_with('~')
712 || version.starts_with('^')
713 || version.starts_with('>')
714 || version.starts_with('<')
715 || version.starts_with('=')
716 || version.starts_with('*')
717 || version.contains("||")
718 || version.contains(" - ")
719 {
720 return false;
721 }
722
723 !is_non_version_dependency(version)
724}
725
726fn is_non_version_dependency(version: &str) -> bool {
727 let version = version.trim();
728
729 version.starts_with("http://")
730 || version.starts_with("https://")
731 || version.starts_with("git://")
732 || version.starts_with("git+ssh://")
733 || version.starts_with("git+http://")
734 || version.starts_with("git+https://")
735 || version.starts_with("git+file://")
736 || version.starts_with("git@")
737 || version.starts_with("file:")
738 || version.starts_with("link:")
739 || version.starts_with("github:")
740 || version.starts_with("gitlab:")
741 || version.starts_with("bitbucket:")
742 || version.starts_with("gist:")
743}
744
745fn non_version_download_url(version: &str, resolved: Option<&str>) -> Option<String> {
746 resolved
747 .map(str::to_string)
748 .or_else(|| match version.trim() {
749 version if version.starts_with("http://") || version.starts_with("https://") => {
750 Some(version.to_string())
751 }
752 _ => None,
753 })
754}
755
756#[allow(clippy::too_many_arguments)]
757fn build_npm_dependency(
758 package_name: &str,
759 version: String,
760 is_dev: bool,
761 is_dev_optional: bool,
762 is_optional: bool,
763 resolved: Option<String>,
764 integrity: Option<&str>,
765 is_direct: bool,
766 from: Option<&str>,
767 in_bundle: bool,
768 nested_deps: Vec<Dependency>,
769) -> Dependency {
770 let (dep_namespace, dep_name) = extract_namespace_and_name(package_name);
771 let dep_namespace = truncate_field(dep_namespace);
772 let dep_name = truncate_field(dep_name);
773 let (scope, is_runtime, is_optional_flag) =
774 determine_scope(is_dev, is_dev_optional, is_optional);
775
776 let alias_spec = parse_npm_alias_spec(&version);
777 let (purl_namespace, purl_name, resolved_version, is_pinned, dep_purl, download_url) =
778 if let Some((alias_namespace, alias_name, alias_constraint)) = alias_spec.clone() {
779 let alias_namespace = truncate_field(alias_namespace);
780 let alias_name = truncate_field(alias_name);
781 let alias_constraint = truncate_field(alias_constraint);
782 let is_pinned = is_exact_version(&alias_constraint);
783 let dep_purl = create_purl(
784 &alias_namespace,
785 &alias_name,
786 is_pinned.then_some(alias_constraint.as_str()),
787 );
788 let download_url = non_version_download_url(&alias_constraint, resolved.as_deref());
789
790 (
791 alias_namespace,
792 alias_name,
793 alias_constraint,
794 is_pinned,
795 dep_purl,
796 download_url,
797 )
798 } else {
799 let is_pinned = is_exact_version(&version);
800 let dep_purl = create_purl(
801 &dep_namespace,
802 &dep_name,
803 is_pinned.then_some(version.as_str()),
804 );
805 let download_url = non_version_download_url(&version, resolved.as_deref());
806
807 (
808 dep_namespace.clone(),
809 dep_name.clone(),
810 version.clone(),
811 is_pinned,
812 dep_purl,
813 download_url,
814 )
815 };
816
817 let (sha1_from_integrity, sha512_from_integrity) = parse_integrity_field(integrity);
818 let sha1_from_url = resolved.as_deref().and_then(parse_resolved_url);
819 let sha1 = sha1_from_integrity.or(sha1_from_url);
820
821 let mut dep_extra_data = HashMap::new();
822 if let Some(from) = from {
823 dep_extra_data.insert("from".to_string(), Value::String(from.to_string()));
824 }
825 if in_bundle {
826 dep_extra_data.insert("inBundle".to_string(), Value::Bool(true));
827 }
828
829 let resolved_package = ResolvedPackage {
830 primary_language: Some("JavaScript".to_string()),
831 download_url,
832 sha1: sha1.and_then(|h| Sha1Digest::from_hex(&h).ok()),
833 sha256: None,
834 sha512: sha512_from_integrity.and_then(|h| Sha512Digest::from_hex(&h).ok()),
835 md5: None,
836 is_virtual: true,
837 extra_data: None,
838 dependencies: nested_deps,
839 repository_homepage_url: None,
840 repository_download_url: None,
841 api_data_url: None,
842 datasource_id: Some(DatasourceId::NpmPackageLockJson),
843 purl: None,
844 ..ResolvedPackage::new(
845 NpmLockParser::PACKAGE_TYPE,
846 purl_namespace,
847 purl_name,
848 resolved_version,
849 )
850 };
851
852 Dependency {
853 purl: dep_purl,
854 extracted_requirement: Some(truncate_field(version)),
855 scope: Some(scope.to_string()),
856 is_runtime: Some(is_runtime),
857 is_optional: Some(is_optional_flag),
858 is_pinned: Some(is_pinned),
859 is_direct: Some(is_direct),
860 resolved_package: Some(Box::new(resolved_package)),
861 extra_data: (!dep_extra_data.is_empty()).then_some(dep_extra_data),
862 }
863}
864
865fn build_link_dependency(
866 package_name: &str,
867 is_dev: bool,
868 is_dev_optional: bool,
869 is_optional: bool,
870 resolved: Option<String>,
871 is_direct: bool,
872) -> Dependency {
873 let (dep_namespace, dep_name) = extract_namespace_and_name(package_name);
874 let dep_namespace = truncate_field(dep_namespace);
875 let dep_name = truncate_field(dep_name);
876 let (scope, is_runtime, is_optional_flag) =
877 determine_scope(is_dev, is_dev_optional, is_optional);
878 let mut extra_data = HashMap::from([("link".to_string(), Value::Bool(true))]);
879
880 if let Some(resolved) = &resolved {
881 extra_data.insert("resolved".to_string(), Value::String(resolved.clone()));
882 }
883
884 Dependency {
885 purl: create_purl(&dep_namespace, &dep_name, None),
886 extracted_requirement: resolved.map(truncate_field),
887 scope: Some(scope.to_string()),
888 is_runtime: Some(is_runtime),
889 is_optional: Some(is_optional_flag),
890 is_pinned: Some(false),
891 is_direct: Some(is_direct),
892 resolved_package: None,
893 extra_data: Some(extra_data),
894 }
895}