1use crate::models::{DatasourceId, Dependency, PackageData, PackageType};
26use log::warn;
27use packageurl::PackageUrl;
28use std::collections::{HashMap, HashSet};
29use std::fs;
30use std::path::Path;
31
32use super::PackageParser;
33
34const PACKAGE_TYPE: PackageType = PackageType::Golang;
35
36pub struct GoModParser;
41
42impl PackageParser for GoModParser {
43 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
44
45 fn extract_packages(path: &Path) -> Vec<PackageData> {
46 let content = match fs::read_to_string(path) {
47 Ok(c) => c,
48 Err(e) => {
49 warn!("Failed to read go.mod at {:?}: {}", path, e);
50 return vec![default_go_mod_package_data()];
51 }
52 };
53
54 vec![parse_go_mod(&content)]
55 }
56
57 fn is_match(path: &Path) -> bool {
58 path.file_name().is_some_and(|name| name == "go.mod")
59 }
60}
61
62#[derive(Debug, Clone, PartialEq)]
63enum BlockState {
64 None,
65 Require,
66 Exclude,
67 Replace,
68 Retract,
69}
70
71pub fn parse_go_mod(content: &str) -> PackageData {
72 let mut namespace: Option<String> = None;
73 let mut name: Option<String> = None;
74 let mut go_version: Option<String> = None;
75 let mut toolchain: Option<String> = None;
76 let mut require_deps: Vec<Dependency> = Vec::new();
77 let mut exclude_deps: Vec<Dependency> = Vec::new();
78 let mut replace_deps: Vec<Dependency> = Vec::new();
79 let mut retracted_versions: Vec<String> = Vec::new();
80 let mut block_state = BlockState::None;
81
82 for line in content.lines() {
83 let trimmed = line.trim();
84
85 if trimmed.is_empty() || trimmed.starts_with("//") {
86 continue;
87 }
88
89 if trimmed == ")" {
91 block_state = BlockState::None;
92 continue;
93 }
94
95 if block_state != BlockState::None {
97 match block_state {
98 BlockState::Require => {
99 if let Some(dep) = parse_dependency_line(trimmed, "require") {
100 require_deps.push(dep);
101 }
102 }
103 BlockState::Exclude => {
104 if let Some(dep) = parse_dependency_line(trimmed, "exclude") {
105 exclude_deps.push(dep);
106 }
107 }
108 BlockState::Replace => {
109 if let Some(dep) = parse_replace_line(trimmed) {
110 replace_deps.push(dep);
111 }
112 }
113 BlockState::Retract => {
114 retracted_versions.extend(parse_retract_value(trimmed));
115 }
116 BlockState::None => {}
117 }
118 continue;
119 }
120
121 if trimmed.starts_with("require") && trimmed.contains('(') {
123 block_state = BlockState::Require;
124 continue;
125 }
126 if trimmed.starts_with("exclude") && trimmed.contains('(') {
127 block_state = BlockState::Exclude;
128 continue;
129 }
130 if trimmed.starts_with("replace") && trimmed.contains('(') {
131 block_state = BlockState::Replace;
132 continue;
133 }
134 if trimmed.starts_with("retract") && trimmed.contains('(') {
135 block_state = BlockState::Retract;
136 continue;
137 }
138
139 if let Some(module_path) = trimmed.strip_prefix("module ") {
141 let module_path = strip_comment(module_path).trim();
142 if !module_path.is_empty() {
143 let (ns, n) = split_module_path(module_path);
144 namespace = ns;
145 name = Some(n);
146 }
147 continue;
148 }
149
150 if let Some(version) = trimmed.strip_prefix("go ") {
152 let version = strip_comment(version).trim();
153 if !version.is_empty() {
154 go_version = Some(version.to_string());
155 }
156 continue;
157 }
158
159 if let Some(tc) = trimmed.strip_prefix("toolchain ") {
161 let tc = strip_comment(tc).trim();
162 if !tc.is_empty() {
163 toolchain = Some(tc.to_string());
164 }
165 continue;
166 }
167
168 if let Some(rest) = trimmed.strip_prefix("require ") {
170 if let Some(dep) = parse_dependency_line(rest, "require") {
171 require_deps.push(dep);
172 }
173 continue;
174 }
175
176 if let Some(rest) = trimmed.strip_prefix("exclude ") {
178 if let Some(dep) = parse_dependency_line(rest, "exclude") {
179 exclude_deps.push(dep);
180 }
181 continue;
182 }
183
184 if let Some(rest) = trimmed.strip_prefix("replace ") {
186 let rest = strip_comment(rest).trim();
187 if !rest.contains('(')
188 && let Some(dep) = parse_replace_line(rest)
189 {
190 replace_deps.push(dep);
191 }
192 continue;
193 }
194
195 if let Some(rest) = trimmed.strip_prefix("retract ") {
197 let rest = strip_comment(rest).trim();
198 if !rest.contains('(') {
199 retracted_versions.extend(parse_retract_value(rest));
200 }
201 continue;
202 }
203 }
204
205 let full_module = match (&namespace, &name) {
206 (Some(ns), Some(n)) => Some(format!("{}/{}", ns, n)),
207 (None, Some(n)) => Some(n.clone()),
208 _ => None,
209 };
210
211 let homepage_url = full_module
212 .as_ref()
213 .map(|m| format!("https://pkg.go.dev/{}", m));
214
215 let vcs_url = full_module.as_ref().map(|m| format!("https://{}.git", m));
216
217 let repository_homepage_url = homepage_url.clone();
218
219 let purl = full_module
220 .as_ref()
221 .and_then(|m| create_golang_purl(m, None));
222
223 let mut dependencies =
224 Vec::with_capacity(require_deps.len() + exclude_deps.len() + replace_deps.len());
225 dependencies.append(&mut require_deps);
226 dependencies.append(&mut exclude_deps);
227 dependencies.append(&mut replace_deps);
228
229 let mut extra_data_map = std::collections::HashMap::new();
230 if let Some(v) = go_version {
231 extra_data_map.insert("go_version".to_string(), serde_json::Value::String(v));
232 }
233 if let Some(tc) = toolchain {
234 extra_data_map.insert("toolchain".to_string(), serde_json::Value::String(tc));
235 }
236 if !retracted_versions.is_empty() {
237 extra_data_map.insert(
238 "retracted_versions".to_string(),
239 serde_json::json!(retracted_versions),
240 );
241 }
242 let extra_data = if extra_data_map.is_empty() {
243 None
244 } else {
245 Some(extra_data_map)
246 };
247
248 PackageData {
249 package_type: Some(PACKAGE_TYPE),
250 namespace,
251 name,
252 version: None,
253 qualifiers: None,
254 subpath: None,
255 primary_language: Some("Go".to_string()),
256 description: None,
257 release_date: None,
258 parties: Vec::new(),
259 keywords: Vec::new(),
260 homepage_url,
261 download_url: None,
262 size: None,
263 sha1: None,
264 md5: None,
265 sha256: None,
266 sha512: None,
267 bug_tracking_url: None,
268 code_view_url: None,
269 vcs_url,
270 copyright: None,
271 holder: None,
272 declared_license_expression: None,
273 declared_license_expression_spdx: None,
274 license_detections: Vec::new(),
275 other_license_expression: None,
276 other_license_expression_spdx: None,
277 other_license_detections: Vec::new(),
278 extracted_license_statement: None,
279 notice_text: None,
280 source_packages: Vec::new(),
281 file_references: Vec::new(),
282 is_private: false,
283 is_virtual: false,
284 extra_data,
285 dependencies,
286 repository_homepage_url,
287 repository_download_url: None,
288 api_data_url: None,
289 datasource_id: Some(DatasourceId::GoMod),
290 purl,
291 }
292}
293
294fn parse_dependency_line(line: &str, scope: &str) -> Option<Dependency> {
303 let trimmed = line.trim();
304 if trimmed.is_empty() || trimmed.starts_with("//") {
305 return None;
306 }
307
308 let is_indirect = trimmed.contains("// indirect");
310 let is_direct = !is_indirect;
311
312 let without_comment = strip_comment(trimmed);
314 let without_comment = without_comment.trim();
315
316 let parts: Vec<&str> = without_comment.split_whitespace().collect();
318 if parts.len() < 2 {
319 return None;
320 }
321
322 let module_path = parts[0];
323 let version = parts[1].to_string();
325
326 let purl = create_golang_purl(module_path, Some(&version));
328
329 Some(Dependency {
330 purl,
331 extracted_requirement: Some(version),
332 scope: Some(scope.to_string()),
333 is_runtime: Some(true),
334 is_optional: Some(false),
335 is_pinned: Some(false),
336 is_direct: Some(is_direct),
337 resolved_package: None,
338 extra_data: None,
339 })
340}
341
342fn parse_replace_line(line: &str) -> Option<Dependency> {
347 let line = strip_comment(line).trim();
348
349 let parts: Vec<&str> = line.splitn(2, "=>").collect();
350 if parts.len() != 2 {
351 return None;
352 }
353
354 let old_parts: Vec<&str> = parts[0].split_whitespace().collect();
355 let new_parts: Vec<&str> = parts[1].split_whitespace().collect();
356
357 if old_parts.is_empty() || new_parts.is_empty() {
358 return None;
359 }
360
361 let old_module = old_parts[0];
362 let old_version = old_parts.get(1).copied();
363 let new_module = new_parts[0];
364 let new_version = new_parts.get(1).map(|s| s.to_string());
365
366 let purl = create_golang_purl(new_module, new_version.as_deref());
367
368 let mut extra = std::collections::HashMap::new();
369 extra.insert(
370 "replace_old".to_string(),
371 serde_json::Value::String(old_module.to_string()),
372 );
373 extra.insert(
374 "replace_new".to_string(),
375 serde_json::Value::String(new_module.to_string()),
376 );
377 if let Some(ref v) = new_version {
378 extra.insert(
379 "replace_version".to_string(),
380 serde_json::Value::String(v.clone()),
381 );
382 }
383 if let Some(ov) = old_version {
384 extra.insert(
385 "replace_old_version".to_string(),
386 serde_json::Value::String(ov.to_string()),
387 );
388 }
389
390 Some(Dependency {
391 purl,
392 extracted_requirement: new_version,
393 scope: Some("replace".to_string()),
394 is_runtime: Some(true),
395 is_optional: Some(false),
396 is_pinned: Some(false),
397 is_direct: Some(true),
398 resolved_package: None,
399 extra_data: Some(extra),
400 })
401}
402
403fn parse_retract_value(value: &str) -> Vec<String> {
405 let trimmed = value.trim();
406 if trimmed.is_empty() {
407 return Vec::new();
408 }
409
410 if trimmed.starts_with('[') && trimmed.ends_with(']') {
411 let inner = &trimmed[1..trimmed.len() - 1];
412 inner
413 .split(',')
414 .map(|s| s.trim().to_string())
415 .filter(|s| !s.is_empty())
416 .collect()
417 } else {
418 vec![trimmed.to_string()]
419 }
420}
421
422pub(crate) fn split_module_path(path: &str) -> (Option<String>, String) {
423 match path.rfind('/') {
424 Some(idx) => {
425 let namespace = &path[..idx];
426 let name = &path[idx + 1..];
427 (Some(namespace.to_string()), name.to_string())
428 }
429 None => (None, path.to_string()),
430 }
431}
432
433fn strip_comment(line: &str) -> &str {
437 match line.find("//") {
438 Some(idx) => &line[..idx],
439 None => line,
440 }
441}
442
443pub(crate) fn create_golang_purl(module_path: &str, version: Option<&str>) -> Option<String> {
448 let (namespace, name) = split_module_path(module_path);
449
450 let mut purl = match PackageUrl::new(PACKAGE_TYPE.as_str(), &name) {
451 Ok(p) => p,
452 Err(e) => {
453 warn!(
454 "Failed to create PURL for golang module '{}': {}",
455 module_path, e
456 );
457 return None;
458 }
459 };
460
461 if let Some(ns) = &namespace
462 && let Err(e) = purl.with_namespace(ns)
463 {
464 warn!(
465 "Failed to set namespace '{}' for golang module '{}': {}",
466 ns, module_path, e
467 );
468 return None;
469 }
470
471 if let Some(v) = version
472 && let Err(e) = purl.with_version(v)
473 {
474 warn!(
475 "Failed to set version '{}' for golang module '{}': {}",
476 v, module_path, e
477 );
478 return None;
479 }
480
481 Some(purl.to_string())
482}
483
484fn default_package_data() -> PackageData {
486 PackageData {
487 package_type: Some(PACKAGE_TYPE),
488 primary_language: Some("Go".to_string()),
489 ..Default::default()
490 }
491}
492
493fn default_go_mod_package_data() -> PackageData {
494 PackageData {
495 datasource_id: Some(DatasourceId::GoMod),
496 ..default_package_data()
497 }
498}
499
500fn default_go_sum_package_data() -> PackageData {
501 PackageData {
502 datasource_id: Some(DatasourceId::GoSum),
503 ..default_package_data()
504 }
505}
506
507fn default_go_work_package_data() -> PackageData {
508 PackageData {
509 datasource_id: Some(DatasourceId::GoWork),
510 ..default_package_data()
511 }
512}
513
514fn default_godeps_package_data() -> PackageData {
515 PackageData {
516 datasource_id: Some(DatasourceId::Godeps),
517 ..default_package_data()
518 }
519}
520
521crate::register_parser!(
522 "Go go.mod module manifest",
523 &["**/go.mod"],
524 "golang",
525 "Go",
526 Some("https://go.dev/ref/mod#go-mod-file"),
527);
528
529pub struct GoSumParser;
534
535impl PackageParser for GoSumParser {
536 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
537
538 fn extract_packages(path: &Path) -> Vec<PackageData> {
539 let content = match fs::read_to_string(path) {
540 Ok(c) => c,
541 Err(e) => {
542 warn!("Failed to read go.sum at {:?}: {}", path, e);
543 return vec![default_go_sum_package_data()];
544 }
545 };
546
547 vec![parse_go_sum(&content)]
548 }
549
550 fn is_match(path: &Path) -> bool {
551 path.file_name().is_some_and(|name| name == "go.sum")
552 }
553}
554
555pub fn parse_go_sum(content: &str) -> PackageData {
556 let mut dependencies = Vec::new();
557 let mut seen = HashSet::new();
558
559 for line in content.lines() {
560 let trimmed = line.trim();
561 if trimmed.is_empty() {
562 continue;
563 }
564
565 let parts: Vec<&str> = trimmed.split_whitespace().collect();
566 if parts.len() < 3 || !parts[2].starts_with("h1:") {
567 continue;
568 }
569
570 let module = parts[0];
571 let raw_version = parts[1];
572
573 let version = raw_version.strip_suffix("/go.mod").unwrap_or(raw_version);
574
575 let key = format!("{}@{}", module, version);
576 if seen.contains(&key) {
577 continue;
578 }
579 seen.insert(key);
580
581 let purl = create_golang_purl(module, Some(version));
582
583 dependencies.push(Dependency {
584 purl,
585 extracted_requirement: Some(version.to_string()),
586 scope: Some("dependency".to_string()),
587 is_runtime: Some(true),
588 is_optional: Some(false),
589 is_pinned: Some(true),
590 is_direct: None,
591 resolved_package: None,
592 extra_data: None,
593 });
594 }
595
596 PackageData {
597 package_type: Some(PACKAGE_TYPE),
598 namespace: None,
599 name: None,
600 version: None,
601 qualifiers: None,
602 subpath: None,
603 primary_language: Some("Go".to_string()),
604 description: None,
605 release_date: None,
606 parties: Vec::new(),
607 keywords: Vec::new(),
608 homepage_url: None,
609 download_url: None,
610 size: None,
611 sha1: None,
612 md5: None,
613 sha256: None,
614 sha512: None,
615 bug_tracking_url: None,
616 code_view_url: None,
617 vcs_url: None,
618 copyright: None,
619 holder: None,
620 declared_license_expression: None,
621 declared_license_expression_spdx: None,
622 license_detections: Vec::new(),
623 other_license_expression: None,
624 other_license_expression_spdx: None,
625 other_license_detections: Vec::new(),
626 extracted_license_statement: None,
627 notice_text: None,
628 source_packages: Vec::new(),
629 file_references: Vec::new(),
630 is_private: false,
631 is_virtual: false,
632 extra_data: None,
633 dependencies,
634 repository_homepage_url: None,
635 repository_download_url: None,
636 api_data_url: None,
637 datasource_id: Some(DatasourceId::GoSum),
638 purl: None,
639 }
640}
641
642crate::register_parser!(
643 "Go go.sum checksum database",
644 &["**/go.sum"],
645 "golang",
646 "Go",
647 Some("https://go.dev/ref/mod#go-sum-files"),
648);
649
650pub struct GoWorkParser;
651
652impl PackageParser for GoWorkParser {
653 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
654
655 fn extract_packages(path: &Path) -> Vec<PackageData> {
656 let content = match fs::read_to_string(path) {
657 Ok(c) => c,
658 Err(e) => {
659 warn!("Failed to read go.work at {:?}: {}", path, e);
660 return vec![default_go_work_package_data()];
661 }
662 };
663
664 vec![parse_go_work(&content, path)]
665 }
666
667 fn is_match(path: &Path) -> bool {
668 path.file_name().is_some_and(|name| name == "go.work")
669 }
670}
671
672pub fn parse_go_work(content: &str, work_path: &Path) -> PackageData {
673 let mut go_version: Option<String> = None;
674 let mut toolchain: Option<String> = None;
675 let mut use_paths: Vec<String> = Vec::new();
676 let mut replace_deps: Vec<Dependency> = Vec::new();
677 let mut unresolved_use_paths: Vec<String> = Vec::new();
678 let mut block_state = BlockState::None;
679
680 for line in content.lines() {
681 let trimmed = line.trim();
682
683 if trimmed.is_empty() || trimmed.starts_with("//") {
684 continue;
685 }
686
687 if trimmed == ")" {
688 block_state = BlockState::None;
689 continue;
690 }
691
692 if block_state != BlockState::None {
693 match block_state {
694 BlockState::Require => {
695 let use_path = extract_single_go_token(trimmed);
696 if let Some(use_path) = use_path.filter(|path| !path.is_empty()) {
697 use_paths.push(use_path.to_string());
698 }
699 }
700 BlockState::Replace => {
701 if let Some(dep) = parse_workspace_replace_line(trimmed) {
702 replace_deps.push(dep);
703 }
704 }
705 _ => {}
706 }
707 continue;
708 }
709
710 if trimmed.starts_with("use") && trimmed.contains('(') {
711 block_state = BlockState::Require;
712 continue;
713 }
714 if trimmed.starts_with("replace") && trimmed.contains('(') {
715 block_state = BlockState::Replace;
716 continue;
717 }
718
719 if let Some(version) = trimmed.strip_prefix("go ") {
720 let version = strip_comment(version).trim();
721 if !version.is_empty() {
722 go_version = Some(version.to_string());
723 }
724 continue;
725 }
726
727 if let Some(tc) = trimmed.strip_prefix("toolchain ") {
728 let tc = strip_comment(tc).trim();
729 if !tc.is_empty() {
730 toolchain = Some(tc.to_string());
731 }
732 continue;
733 }
734
735 if let Some(rest) = trimmed.strip_prefix("use ") {
736 let use_path = extract_single_go_token(rest);
737 if let Some(use_path) = use_path.filter(|path| !path.is_empty()) {
738 use_paths.push(use_path.to_string());
739 }
740 continue;
741 }
742
743 if let Some(rest) = trimmed.strip_prefix("replace ") {
744 if let Some(dep) = parse_workspace_replace_line(rest) {
745 replace_deps.push(dep);
746 }
747 continue;
748 }
749 }
750
751 if go_version.is_none() || use_paths.is_empty() {
752 warn!("Invalid go.work: missing go directive or use directive");
753 return default_go_work_package_data();
754 }
755
756 let (mut dependencies, unresolved) = resolve_workspace_use_dependencies(work_path, &use_paths);
757 dependencies.extend(replace_deps);
758 unresolved_use_paths.extend(unresolved);
759
760 let mut extra_data = HashMap::new();
761 if let Some(v) = go_version {
762 extra_data.insert("go_version".to_string(), serde_json::Value::String(v));
763 }
764 if let Some(tc) = toolchain {
765 extra_data.insert("toolchain".to_string(), serde_json::Value::String(tc));
766 }
767 extra_data.insert(
768 "use_paths".to_string(),
769 serde_json::Value::Array(
770 use_paths
771 .iter()
772 .cloned()
773 .map(serde_json::Value::String)
774 .collect(),
775 ),
776 );
777 if !unresolved_use_paths.is_empty() {
778 extra_data.insert(
779 "unresolved_use_paths".to_string(),
780 serde_json::Value::Array(
781 unresolved_use_paths
782 .into_iter()
783 .map(serde_json::Value::String)
784 .collect(),
785 ),
786 );
787 }
788
789 PackageData {
790 package_type: Some(PACKAGE_TYPE),
791 namespace: None,
792 name: None,
793 version: None,
794 qualifiers: None,
795 subpath: None,
796 primary_language: Some("Go".to_string()),
797 description: None,
798 release_date: None,
799 parties: Vec::new(),
800 keywords: Vec::new(),
801 homepage_url: None,
802 download_url: None,
803 size: None,
804 sha1: None,
805 md5: None,
806 sha256: None,
807 sha512: None,
808 bug_tracking_url: None,
809 code_view_url: None,
810 vcs_url: None,
811 copyright: None,
812 holder: None,
813 declared_license_expression: None,
814 declared_license_expression_spdx: None,
815 license_detections: Vec::new(),
816 other_license_expression: None,
817 other_license_expression_spdx: None,
818 other_license_detections: Vec::new(),
819 extracted_license_statement: None,
820 notice_text: None,
821 source_packages: Vec::new(),
822 file_references: Vec::new(),
823 is_private: false,
824 is_virtual: false,
825 extra_data: Some(extra_data),
826 dependencies,
827 repository_homepage_url: None,
828 repository_download_url: None,
829 api_data_url: None,
830 datasource_id: Some(DatasourceId::GoWork),
831 purl: None,
832 }
833}
834
835fn resolve_workspace_use_dependencies(
836 work_path: &Path,
837 use_paths: &[String],
838) -> (Vec<Dependency>, Vec<String>) {
839 let Some(base_dir) = work_path.parent() else {
840 return (Vec::new(), use_paths.to_vec());
841 };
842
843 let mut dependencies = Vec::new();
844 let mut unresolved = Vec::new();
845
846 for use_path in use_paths {
847 let go_mod_path = base_dir.join(use_path).join("go.mod");
848 let module_path = fs::read_to_string(&go_mod_path)
849 .ok()
850 .and_then(|content| extract_module_path_from_go_mod(&content));
851
852 let purl = module_path
853 .as_deref()
854 .and_then(|module_path| create_golang_purl(module_path, None));
855
856 if purl.is_none() {
857 unresolved.push(use_path.clone());
858 continue;
859 }
860
861 let mut extra_data = HashMap::new();
862 extra_data.insert(
863 "workspace_path".to_string(),
864 serde_json::Value::String(use_path.clone()),
865 );
866 if let Some(module_path) = module_path {
867 extra_data.insert(
868 "workspace_module_path".to_string(),
869 serde_json::Value::String(module_path),
870 );
871 }
872
873 dependencies.push(Dependency {
874 purl,
875 extracted_requirement: Some(use_path.clone()),
876 scope: Some("use".to_string()),
877 is_runtime: Some(true),
878 is_optional: Some(false),
879 is_pinned: Some(false),
880 is_direct: Some(true),
881 resolved_package: None,
882 extra_data: Some(extra_data),
883 });
884 }
885
886 (dependencies, unresolved)
887}
888
889fn extract_module_path_from_go_mod(content: &str) -> Option<String> {
890 for line in content.lines() {
891 let trimmed = line.trim();
892 if let Some(module_path) = trimmed.strip_prefix("module ") {
893 let module_path = strip_comment(module_path).trim();
894 if !module_path.is_empty() {
895 return Some(module_path.to_string());
896 }
897 }
898 }
899 None
900}
901
902fn parse_workspace_replace_line(line: &str) -> Option<Dependency> {
903 let line = strip_comment(line).trim();
904 let parts: Vec<&str> = line.splitn(2, "=>").collect();
905 if parts.len() != 2 {
906 return None;
907 }
908
909 let old_parts = parse_go_tokens(parts[0]);
910 let new_parts = parse_go_tokens(parts[1]);
911 if old_parts.is_empty() || new_parts.is_empty() {
912 return None;
913 }
914
915 let old_module = old_parts[0].as_str();
916 let old_version = old_parts.get(1).map(|s| s.as_str());
917 let new_module = new_parts[0].as_str();
918 let new_version = new_parts.get(1).cloned();
919 let is_local_path = new_module.starts_with("./")
920 || new_module.starts_with("../")
921 || new_module.starts_with('/')
922 || new_module.starts_with('~');
923
924 let purl = if is_local_path {
925 None
926 } else {
927 create_golang_purl(new_module, new_version.as_deref())
928 };
929
930 let mut extra = std::collections::HashMap::new();
931 extra.insert(
932 "replace_old".to_string(),
933 serde_json::Value::String(old_module.to_string()),
934 );
935 extra.insert(
936 "replace_new".to_string(),
937 serde_json::Value::String(new_module.to_string()),
938 );
939 if let Some(ref v) = new_version {
940 extra.insert(
941 "replace_version".to_string(),
942 serde_json::Value::String(v.clone()),
943 );
944 }
945 if let Some(ov) = old_version {
946 extra.insert(
947 "replace_old_version".to_string(),
948 serde_json::Value::String(ov.to_string()),
949 );
950 }
951 if is_local_path {
952 extra.insert(
953 "replace_local_path".to_string(),
954 serde_json::Value::Bool(true),
955 );
956 }
957
958 Some(Dependency {
959 purl,
960 extracted_requirement: new_version,
961 scope: Some("replace".to_string()),
962 is_runtime: Some(true),
963 is_optional: Some(false),
964 is_pinned: Some(false),
965 is_direct: Some(true),
966 resolved_package: None,
967 extra_data: Some(extra),
968 })
969}
970
971fn extract_single_go_token(value: &str) -> Option<String> {
972 parse_go_tokens(value).into_iter().next()
973}
974
975fn parse_go_tokens(value: &str) -> Vec<String> {
976 let mut tokens = Vec::new();
977 let mut current = String::new();
978 let mut quote: Option<char> = None;
979 let mut chars = value.chars().peekable();
980
981 while let Some(ch) = chars.next() {
982 if let Some(active_quote) = quote {
983 if ch == active_quote {
984 quote = None;
985 continue;
986 }
987
988 if active_quote == '"' && ch == '\\' {
989 if let Some(next) = chars.next() {
990 current.push(next);
991 }
992 continue;
993 }
994
995 current.push(ch);
996 continue;
997 }
998
999 match ch {
1000 '"' | '`' => {
1001 quote = Some(ch);
1002 }
1003 c if c.is_whitespace() => {
1004 if !current.is_empty() {
1005 tokens.push(std::mem::take(&mut current));
1006 }
1007 }
1008 _ => current.push(ch),
1009 }
1010 }
1011
1012 if !current.is_empty() {
1013 tokens.push(current);
1014 }
1015
1016 tokens
1017}
1018
1019crate::register_parser!(
1020 "Go go.work workspace file",
1021 &["**/go.work"],
1022 "golang",
1023 "Go",
1024 Some("https://go.dev/ref/mod#go-work-files"),
1025);
1026
1027pub struct GodepsParser;
1032
1033impl PackageParser for GodepsParser {
1034 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
1035
1036 fn extract_packages(path: &Path) -> Vec<PackageData> {
1037 let content = match fs::read_to_string(path) {
1038 Ok(c) => c,
1039 Err(e) => {
1040 warn!("Failed to read Godeps.json at {:?}: {}", path, e);
1041 return vec![default_godeps_package_data()];
1042 }
1043 };
1044
1045 vec![parse_godeps_json(&content)]
1046 }
1047
1048 fn is_match(path: &Path) -> bool {
1049 path.file_name().is_some_and(|name| name == "Godeps.json")
1050 }
1051}
1052
1053pub fn parse_godeps_json(content: &str) -> PackageData {
1054 let json: serde_json::Value = match serde_json::from_str(content) {
1055 Ok(j) => j,
1056 Err(e) => {
1057 warn!("Failed to parse Godeps.json: {}", e);
1058 return default_godeps_package_data();
1059 }
1060 };
1061
1062 let import_path = json
1063 .get("ImportPath")
1064 .and_then(|v| v.as_str())
1065 .map(String::from);
1066
1067 let go_version = json
1068 .get("GoVersion")
1069 .and_then(|v| v.as_str())
1070 .map(String::from);
1071
1072 let (namespace, name) = match &import_path {
1073 Some(ip) => {
1074 let (ns, n) = split_module_path(ip);
1075 (ns, Some(n))
1076 }
1077 None => (None, None),
1078 };
1079
1080 let purl = import_path
1081 .as_deref()
1082 .and_then(|ip| create_golang_purl(ip, None));
1083
1084 let mut dependencies = Vec::new();
1085
1086 if let Some(deps) = json.get("Deps").and_then(|v| v.as_array()) {
1087 for dep in deps {
1088 let dep_import_path = dep.get("ImportPath").and_then(|v| v.as_str());
1089 let rev = dep.get("Rev").and_then(|v| v.as_str());
1090
1091 if let Some(path) = dep_import_path {
1092 let dep_purl = create_golang_purl(path, None);
1093
1094 dependencies.push(Dependency {
1095 purl: dep_purl,
1096 extracted_requirement: rev.map(String::from),
1097 scope: Some("Deps".to_string()),
1098 is_runtime: Some(true),
1099 is_optional: Some(false),
1100 is_pinned: Some(false),
1101 is_direct: None,
1102 resolved_package: None,
1103 extra_data: None,
1104 });
1105 }
1106 }
1107 }
1108
1109 let extra_data = go_version.map(|v| {
1110 let mut map = HashMap::new();
1111 map.insert("go_version".to_string(), serde_json::Value::String(v));
1112 map
1113 });
1114
1115 let homepage_url = import_path
1116 .as_ref()
1117 .map(|m| format!("https://pkg.go.dev/{}", m));
1118
1119 let vcs_url = import_path.as_ref().map(|m| format!("https://{}.git", m));
1120
1121 PackageData {
1122 package_type: Some(PACKAGE_TYPE),
1123 namespace,
1124 name,
1125 version: None,
1126 qualifiers: None,
1127 subpath: None,
1128 primary_language: Some("Go".to_string()),
1129 description: None,
1130 release_date: None,
1131 parties: Vec::new(),
1132 keywords: Vec::new(),
1133 homepage_url,
1134 download_url: None,
1135 size: None,
1136 sha1: None,
1137 md5: None,
1138 sha256: None,
1139 sha512: None,
1140 bug_tracking_url: None,
1141 code_view_url: None,
1142 vcs_url,
1143 copyright: None,
1144 holder: None,
1145 declared_license_expression: None,
1146 declared_license_expression_spdx: None,
1147 license_detections: Vec::new(),
1148 other_license_expression: None,
1149 other_license_expression_spdx: None,
1150 other_license_detections: Vec::new(),
1151 extracted_license_statement: None,
1152 notice_text: None,
1153 source_packages: Vec::new(),
1154 file_references: Vec::new(),
1155 is_private: false,
1156 is_virtual: false,
1157 extra_data,
1158 dependencies,
1159 repository_homepage_url: None,
1160 repository_download_url: None,
1161 api_data_url: None,
1162 datasource_id: Some(DatasourceId::Godeps),
1163 purl,
1164 }
1165}
1166
1167crate::register_parser!(
1168 "Go Godeps.json legacy dependency file",
1169 &["**/Godeps.json"],
1170 "golang",
1171 "Go",
1172 None,
1173);