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