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