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