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