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 purl = create_golang_purl(new_module, new_version.as_deref());
381
382 let mut extra = std::collections::HashMap::new();
383 extra.insert(
384 "replace_old".to_string(),
385 serde_json::Value::String(truncate_field(old_module.to_string())),
386 );
387 extra.insert(
388 "replace_new".to_string(),
389 serde_json::Value::String(truncate_field(new_module.to_string())),
390 );
391 if let Some(ref v) = new_version {
392 extra.insert(
393 "replace_version".to_string(),
394 serde_json::Value::String(v.clone()),
395 );
396 }
397 if let Some(ov) = old_version {
398 extra.insert(
399 "replace_old_version".to_string(),
400 serde_json::Value::String(truncate_field(ov.to_string())),
401 );
402 }
403
404 Some(Dependency {
405 purl,
406 extracted_requirement: new_version,
407 scope: Some("replace".to_string()),
408 is_runtime: Some(true),
409 is_optional: Some(false),
410 is_pinned: Some(false),
411 is_direct: Some(true),
412 resolved_package: None,
413 extra_data: Some(extra),
414 })
415}
416
417fn parse_retract_value(value: &str) -> Vec<String> {
419 let trimmed = value.trim();
420 if trimmed.is_empty() {
421 return Vec::new();
422 }
423
424 if trimmed.starts_with('[') && trimmed.ends_with(']') {
425 let inner = &trimmed[1..trimmed.len() - 1];
426 inner
427 .split(',')
428 .map(|s| s.trim().to_string())
429 .filter(|s| !s.is_empty())
430 .collect()
431 } else {
432 vec![trimmed.to_string()]
433 }
434}
435
436pub(crate) fn split_module_path(path: &str) -> (Option<String>, String) {
437 match path.rfind('/') {
438 Some(idx) => {
439 let namespace = &path[..idx];
440 let name = &path[idx + 1..];
441 (
442 Some(truncate_field(namespace.to_string())),
443 truncate_field(name.to_string()),
444 )
445 }
446 None => (None, truncate_field(path.to_string())),
447 }
448}
449
450fn strip_comment(line: &str) -> &str {
454 match line.find("//") {
455 Some(idx) => &line[..idx],
456 None => line,
457 }
458}
459
460pub(crate) fn create_golang_purl(module_path: &str, version: Option<&str>) -> Option<String> {
465 let (namespace, name) = split_module_path(module_path);
466
467 let mut purl = match PackageUrl::new(PACKAGE_TYPE.as_str(), &name) {
468 Ok(p) => p,
469 Err(e) => {
470 warn!(
471 "Failed to create PURL for golang module '{}': {}",
472 module_path, e
473 );
474 return None;
475 }
476 };
477
478 if let Some(ns) = &namespace
479 && let Err(e) = purl.with_namespace(ns)
480 {
481 warn!(
482 "Failed to set namespace '{}' for golang module '{}': {}",
483 ns, module_path, e
484 );
485 return None;
486 }
487
488 if let Some(v) = version
489 && let Err(e) = purl.with_version(v)
490 {
491 warn!(
492 "Failed to set version '{}' for golang module '{}': {}",
493 v, module_path, e
494 );
495 return None;
496 }
497
498 Some(purl.to_string())
499}
500
501fn default_package_data() -> PackageData {
503 PackageData {
504 package_type: Some(PACKAGE_TYPE),
505 primary_language: Some("Go".to_string()),
506 ..Default::default()
507 }
508}
509
510fn default_go_mod_package_data() -> PackageData {
511 PackageData {
512 datasource_id: Some(DatasourceId::GoMod),
513 ..default_package_data()
514 }
515}
516
517fn default_go_sum_package_data() -> PackageData {
518 PackageData {
519 datasource_id: Some(DatasourceId::GoSum),
520 ..default_package_data()
521 }
522}
523
524fn default_go_work_package_data() -> PackageData {
525 PackageData {
526 datasource_id: Some(DatasourceId::GoWork),
527 ..default_package_data()
528 }
529}
530
531fn default_godeps_package_data() -> PackageData {
532 PackageData {
533 datasource_id: Some(DatasourceId::Godeps),
534 ..default_package_data()
535 }
536}
537
538pub struct GoSumParser;
543
544impl PackageParser for GoSumParser {
545 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
546
547 fn metadata() -> Vec<ParserMetadata> {
548 vec![ParserMetadata {
549 description: "Go go.sum checksum database",
550 file_patterns: &["**/go.sum"],
551 package_type: "golang",
552 primary_language: "Go",
553 documentation_url: Some("https://go.dev/ref/mod#go-sum-files"),
554 }]
555 }
556
557 fn extract_packages(path: &Path) -> Vec<PackageData> {
558 let content = match read_file_to_string(path, None) {
559 Ok(c) => c,
560 Err(e) => {
561 warn!("Failed to read go.sum at {:?}: {}", path, e);
562 return vec![default_go_sum_package_data()];
563 }
564 };
565
566 vec![parse_go_sum(&content)]
567 }
568
569 fn is_match(path: &Path) -> bool {
570 path.file_name().is_some_and(|name| name == "go.sum")
571 }
572}
573
574pub fn parse_go_sum(content: &str) -> PackageData {
575 let mut dependencies = Vec::new();
576 let mut seen = HashSet::new();
577
578 for line in content.lines().take(MAX_ITERATION_COUNT) {
579 let trimmed = line.trim();
580 if trimmed.is_empty() {
581 continue;
582 }
583
584 let parts: Vec<&str> = trimmed.split_whitespace().collect();
585 if parts.len() < 3 || !parts[2].starts_with("h1:") {
586 continue;
587 }
588
589 let module = parts[0];
590 let raw_version = parts[1];
591
592 let version = raw_version.strip_suffix("/go.mod").unwrap_or(raw_version);
593
594 let key = format!("{}@{}", module, version);
595 if seen.contains(&key) {
596 continue;
597 }
598 seen.insert(key);
599
600 let purl = create_golang_purl(module, Some(version));
601
602 dependencies.push(Dependency {
603 purl,
604 extracted_requirement: Some(truncate_field(version.to_string())),
605 scope: Some("dependency".to_string()),
606 is_runtime: Some(true),
607 is_optional: Some(false),
608 is_pinned: Some(true),
609 is_direct: None,
610 resolved_package: None,
611 extra_data: None,
612 });
613 }
614
615 PackageData {
616 package_type: Some(PACKAGE_TYPE),
617 namespace: None,
618 name: None,
619 version: None,
620 qualifiers: None,
621 subpath: None,
622 primary_language: Some("Go".to_string()),
623 description: None,
624 release_date: None,
625 parties: Vec::new(),
626 keywords: Vec::new(),
627 homepage_url: None,
628 download_url: None,
629 size: None,
630 sha1: None,
631 md5: None,
632 sha256: None,
633 sha512: None,
634 bug_tracking_url: None,
635 code_view_url: None,
636 vcs_url: None,
637 copyright: None,
638 holder: None,
639 declared_license_expression: None,
640 declared_license_expression_spdx: None,
641 license_detections: Vec::new(),
642 other_license_expression: None,
643 other_license_expression_spdx: None,
644 other_license_detections: Vec::new(),
645 extracted_license_statement: None,
646 notice_text: None,
647 source_packages: Vec::new(),
648 file_references: Vec::new(),
649 is_private: false,
650 is_virtual: false,
651 extra_data: None,
652 dependencies,
653 repository_homepage_url: None,
654 repository_download_url: None,
655 api_data_url: None,
656 datasource_id: Some(DatasourceId::GoSum),
657 purl: None,
658 }
659}
660
661pub struct GoWorkParser;
662
663impl PackageParser for GoWorkParser {
664 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
665
666 fn metadata() -> Vec<ParserMetadata> {
667 vec![ParserMetadata {
668 description: "Go go.work workspace file",
669 file_patterns: &["**/go.work"],
670 package_type: "golang",
671 primary_language: "Go",
672 documentation_url: Some("https://go.dev/ref/mod#go-work-files"),
673 }]
674 }
675
676 fn extract_packages(path: &Path) -> Vec<PackageData> {
677 let content = match read_file_to_string(path, None) {
678 Ok(c) => c,
679 Err(e) => {
680 warn!("Failed to read go.work at {:?}: {}", path, e);
681 return vec![default_go_work_package_data()];
682 }
683 };
684
685 vec![parse_go_work(&content, path)]
686 }
687
688 fn is_match(path: &Path) -> bool {
689 path.file_name().is_some_and(|name| name == "go.work")
690 }
691}
692
693pub fn parse_go_work(content: &str, work_path: &Path) -> PackageData {
694 let mut go_version: Option<String> = None;
695 let mut toolchain: Option<String> = None;
696 let mut use_paths: Vec<String> = Vec::new();
697 let mut replace_deps: Vec<Dependency> = Vec::new();
698 let mut unresolved_use_paths: Vec<String> = Vec::new();
699 let mut block_state = BlockState::None;
700
701 for line in content.lines().take(MAX_ITERATION_COUNT) {
702 let trimmed = line.trim();
703
704 if trimmed.is_empty() || trimmed.starts_with("//") {
705 continue;
706 }
707
708 if trimmed == ")" {
709 block_state = BlockState::None;
710 continue;
711 }
712
713 if block_state != BlockState::None {
714 match block_state {
715 BlockState::Require => {
716 let use_path = extract_single_go_token(trimmed);
717 if let Some(use_path) = use_path.filter(|path| !path.is_empty()) {
718 use_paths.push(truncate_field(use_path));
719 }
720 }
721 BlockState::Replace => {
722 if let Some(dep) = parse_workspace_replace_line(trimmed) {
723 replace_deps.push(dep);
724 }
725 }
726 _ => {}
727 }
728 continue;
729 }
730
731 if trimmed.starts_with("use") && trimmed.contains('(') {
732 block_state = BlockState::Require;
733 continue;
734 }
735 if trimmed.starts_with("replace") && trimmed.contains('(') {
736 block_state = BlockState::Replace;
737 continue;
738 }
739
740 if let Some(version) = trimmed.strip_prefix("go ") {
741 let version = strip_comment(version).trim();
742 if !version.is_empty() {
743 go_version = Some(truncate_field(version.to_string()));
744 }
745 continue;
746 }
747
748 if let Some(tc) = trimmed.strip_prefix("toolchain ") {
749 let tc = strip_comment(tc).trim();
750 if !tc.is_empty() {
751 toolchain = Some(truncate_field(tc.to_string()));
752 }
753 continue;
754 }
755
756 if let Some(rest) = trimmed.strip_prefix("use ") {
757 let use_path = extract_single_go_token(rest);
758 if let Some(use_path) = use_path.filter(|path| !path.is_empty()) {
759 use_paths.push(truncate_field(use_path));
760 }
761 continue;
762 }
763
764 if let Some(rest) = trimmed.strip_prefix("replace ") {
765 if let Some(dep) = parse_workspace_replace_line(rest) {
766 replace_deps.push(dep);
767 }
768 continue;
769 }
770 }
771
772 if go_version.is_none() || use_paths.is_empty() {
773 warn!("Invalid go.work: missing go directive or use directive");
774 return default_go_work_package_data();
775 }
776
777 let (mut dependencies, unresolved) = resolve_workspace_use_dependencies(work_path, &use_paths);
778 dependencies.extend(replace_deps);
779 unresolved_use_paths.extend(unresolved);
780
781 let mut extra_data = HashMap::new();
782 if let Some(v) = go_version {
783 extra_data.insert("go_version".to_string(), serde_json::Value::String(v));
784 }
785 if let Some(tc) = toolchain {
786 extra_data.insert("toolchain".to_string(), serde_json::Value::String(tc));
787 }
788 extra_data.insert(
789 "use_paths".to_string(),
790 serde_json::Value::Array(
791 use_paths
792 .iter()
793 .cloned()
794 .map(serde_json::Value::String)
795 .collect(),
796 ),
797 );
798 if !unresolved_use_paths.is_empty() {
799 extra_data.insert(
800 "unresolved_use_paths".to_string(),
801 serde_json::Value::Array(
802 unresolved_use_paths
803 .into_iter()
804 .map(serde_json::Value::String)
805 .collect(),
806 ),
807 );
808 }
809
810 PackageData {
811 package_type: Some(PACKAGE_TYPE),
812 namespace: None,
813 name: None,
814 version: None,
815 qualifiers: None,
816 subpath: None,
817 primary_language: Some("Go".to_string()),
818 description: None,
819 release_date: None,
820 parties: Vec::new(),
821 keywords: Vec::new(),
822 homepage_url: None,
823 download_url: None,
824 size: None,
825 sha1: None,
826 md5: None,
827 sha256: None,
828 sha512: None,
829 bug_tracking_url: None,
830 code_view_url: None,
831 vcs_url: None,
832 copyright: None,
833 holder: None,
834 declared_license_expression: None,
835 declared_license_expression_spdx: None,
836 license_detections: Vec::new(),
837 other_license_expression: None,
838 other_license_expression_spdx: None,
839 other_license_detections: Vec::new(),
840 extracted_license_statement: None,
841 notice_text: None,
842 source_packages: Vec::new(),
843 file_references: Vec::new(),
844 is_private: false,
845 is_virtual: false,
846 extra_data: Some(extra_data),
847 dependencies,
848 repository_homepage_url: None,
849 repository_download_url: None,
850 api_data_url: None,
851 datasource_id: Some(DatasourceId::GoWork),
852 purl: None,
853 }
854}
855
856fn resolve_workspace_use_dependencies(
857 work_path: &Path,
858 use_paths: &[String],
859) -> (Vec<Dependency>, Vec<String>) {
860 let Some(base_dir) = work_path.parent() else {
861 return (Vec::new(), use_paths.to_vec());
862 };
863
864 let mut dependencies = Vec::new();
865 let mut unresolved = Vec::new();
866
867 for use_path in use_paths.iter().take(MAX_ITERATION_COUNT) {
868 let go_mod_path = base_dir.join(use_path).join("go.mod");
869 let module_path = read_file_to_string(&go_mod_path, None)
870 .ok()
871 .and_then(|content| extract_module_path_from_go_mod(&content));
872
873 let purl = module_path
874 .as_deref()
875 .and_then(|module_path| create_golang_purl(module_path, None));
876
877 if purl.is_none() {
878 unresolved.push(use_path.clone());
879 continue;
880 }
881
882 let mut extra_data = HashMap::new();
883 extra_data.insert(
884 "workspace_path".to_string(),
885 serde_json::Value::String(truncate_field(use_path.clone())),
886 );
887 if let Some(module_path) = module_path {
888 extra_data.insert(
889 "workspace_module_path".to_string(),
890 serde_json::Value::String(truncate_field(module_path)),
891 );
892 }
893
894 dependencies.push(Dependency {
895 purl,
896 extracted_requirement: Some(truncate_field(use_path.clone())),
897 scope: Some("use".to_string()),
898 is_runtime: Some(true),
899 is_optional: Some(false),
900 is_pinned: Some(false),
901 is_direct: Some(true),
902 resolved_package: None,
903 extra_data: Some(extra_data),
904 });
905 }
906
907 (dependencies, unresolved)
908}
909
910fn extract_module_path_from_go_mod(content: &str) -> Option<String> {
911 for line in content.lines().take(MAX_ITERATION_COUNT) {
912 let trimmed = line.trim();
913 if let Some(module_path) = trimmed.strip_prefix("module ") {
914 let module_path = strip_comment(module_path).trim();
915 if !module_path.is_empty() {
916 return Some(truncate_field(module_path.to_string()));
917 }
918 }
919 }
920 None
921}
922
923fn parse_workspace_replace_line(line: &str) -> Option<Dependency> {
924 let line = strip_comment(line).trim();
925 let parts: Vec<&str> = line.splitn(2, "=>").collect();
926 if parts.len() != 2 {
927 return None;
928 }
929
930 let old_parts = parse_go_tokens(parts[0]);
931 let new_parts = parse_go_tokens(parts[1]);
932 if old_parts.is_empty() || new_parts.is_empty() {
933 return None;
934 }
935
936 let old_module = old_parts[0].as_str();
937 let old_version = old_parts.get(1).map(|s| s.as_str());
938 let new_module = new_parts[0].as_str();
939 let new_version = new_parts.get(1).map(|s| truncate_field(s.clone()));
940 let is_local_path = new_module.starts_with("./")
941 || new_module.starts_with("../")
942 || new_module.starts_with('/')
943 || new_module.starts_with('~');
944
945 let purl = if is_local_path {
946 None
947 } else {
948 create_golang_purl(new_module, new_version.as_deref())
949 };
950
951 let mut extra = std::collections::HashMap::new();
952 extra.insert(
953 "replace_old".to_string(),
954 serde_json::Value::String(truncate_field(old_module.to_string())),
955 );
956 extra.insert(
957 "replace_new".to_string(),
958 serde_json::Value::String(truncate_field(new_module.to_string())),
959 );
960 if let Some(ref v) = new_version {
961 extra.insert(
962 "replace_version".to_string(),
963 serde_json::Value::String(v.clone()),
964 );
965 }
966 if let Some(ov) = old_version {
967 extra.insert(
968 "replace_old_version".to_string(),
969 serde_json::Value::String(truncate_field(ov.to_string())),
970 );
971 }
972 if is_local_path {
973 extra.insert(
974 "replace_local_path".to_string(),
975 serde_json::Value::Bool(true),
976 );
977 }
978
979 Some(Dependency {
980 purl,
981 extracted_requirement: new_version,
982 scope: Some("replace".to_string()),
983 is_runtime: Some(true),
984 is_optional: Some(false),
985 is_pinned: Some(false),
986 is_direct: Some(true),
987 resolved_package: None,
988 extra_data: Some(extra),
989 })
990}
991
992fn extract_single_go_token(value: &str) -> Option<String> {
993 parse_go_tokens(value).into_iter().next()
994}
995
996fn parse_go_tokens(value: &str) -> Vec<String> {
997 let mut tokens = Vec::new();
998 let mut current = String::new();
999 let mut quote: Option<char> = None;
1000 let mut chars = value.chars().peekable();
1001
1002 while let Some(ch) = chars.next() {
1003 if let Some(active_quote) = quote {
1004 if ch == active_quote {
1005 quote = None;
1006 continue;
1007 }
1008
1009 if active_quote == '"' && ch == '\\' {
1010 if let Some(next) = chars.next() {
1011 current.push(next);
1012 }
1013 continue;
1014 }
1015
1016 current.push(ch);
1017 continue;
1018 }
1019
1020 match ch {
1021 '"' | '`' => {
1022 quote = Some(ch);
1023 }
1024 c if c.is_whitespace() => {
1025 if !current.is_empty() {
1026 tokens.push(std::mem::take(&mut current));
1027 }
1028 }
1029 _ => current.push(ch),
1030 }
1031 }
1032
1033 if !current.is_empty() {
1034 tokens.push(current);
1035 }
1036
1037 tokens
1038}
1039
1040pub struct GodepsParser;
1045
1046impl PackageParser for GodepsParser {
1047 const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
1048
1049 fn metadata() -> Vec<ParserMetadata> {
1050 vec![ParserMetadata {
1051 description: "Go Godeps.json legacy dependency file",
1052 file_patterns: &["**/Godeps.json"],
1053 package_type: "golang",
1054 primary_language: "Go",
1055 documentation_url: None,
1056 }]
1057 }
1058
1059 fn extract_packages(path: &Path) -> Vec<PackageData> {
1060 let content = match read_file_to_string(path, None) {
1061 Ok(c) => c,
1062 Err(e) => {
1063 warn!("Failed to read Godeps.json at {:?}: {}", path, e);
1064 return vec![default_godeps_package_data()];
1065 }
1066 };
1067
1068 vec![parse_godeps_json(&content)]
1069 }
1070
1071 fn is_match(path: &Path) -> bool {
1072 path.file_name().is_some_and(|name| name == "Godeps.json")
1073 }
1074}
1075
1076pub fn parse_godeps_json(content: &str) -> PackageData {
1077 let json: serde_json::Value = match serde_json::from_str(content) {
1078 Ok(j) => j,
1079 Err(e) => {
1080 warn!("Failed to parse Godeps.json: {}", e);
1081 return default_godeps_package_data();
1082 }
1083 };
1084
1085 let import_path = json
1086 .get("ImportPath")
1087 .and_then(|v| v.as_str())
1088 .map(|s| truncate_field(s.to_string()));
1089
1090 let go_version = json
1091 .get("GoVersion")
1092 .and_then(|v| v.as_str())
1093 .map(|s| truncate_field(s.to_string()));
1094
1095 let (namespace, name) = match &import_path {
1096 Some(ip) => {
1097 let (ns, n) = split_module_path(ip);
1098 (ns, Some(n))
1099 }
1100 None => (None, None),
1101 };
1102
1103 let purl = import_path
1104 .as_deref()
1105 .and_then(|ip| create_golang_purl(ip, None));
1106
1107 let mut dependencies = Vec::new();
1108
1109 if let Some(deps) = json.get("Deps").and_then(|v| v.as_array()) {
1110 for dep in deps.iter().take(MAX_ITERATION_COUNT) {
1111 let dep_import_path = dep.get("ImportPath").and_then(|v| v.as_str());
1112 let rev = dep.get("Rev").and_then(|v| v.as_str());
1113
1114 if let Some(path) = dep_import_path {
1115 let dep_purl = create_golang_purl(path, None);
1116
1117 dependencies.push(Dependency {
1118 purl: dep_purl,
1119 extracted_requirement: rev.map(|s| truncate_field(s.to_string())),
1120 scope: Some("Deps".to_string()),
1121 is_runtime: Some(true),
1122 is_optional: Some(false),
1123 is_pinned: Some(false),
1124 is_direct: None,
1125 resolved_package: None,
1126 extra_data: None,
1127 });
1128 }
1129 }
1130 }
1131
1132 let extra_data = go_version.map(|v| {
1133 let mut map = HashMap::new();
1134 map.insert("go_version".to_string(), serde_json::Value::String(v));
1135 map
1136 });
1137
1138 let homepage_url = import_path
1139 .as_ref()
1140 .map(|m| truncate_field(format!("https://pkg.go.dev/{}", m)));
1141
1142 let vcs_url = import_path
1143 .as_ref()
1144 .map(|m| truncate_field(format!("https://{}.git", m)));
1145
1146 PackageData {
1147 package_type: Some(PACKAGE_TYPE),
1148 namespace,
1149 name,
1150 version: None,
1151 qualifiers: None,
1152 subpath: None,
1153 primary_language: Some("Go".to_string()),
1154 description: None,
1155 release_date: None,
1156 parties: Vec::new(),
1157 keywords: Vec::new(),
1158 homepage_url,
1159 download_url: None,
1160 size: None,
1161 sha1: None,
1162 md5: None,
1163 sha256: None,
1164 sha512: None,
1165 bug_tracking_url: None,
1166 code_view_url: None,
1167 vcs_url,
1168 copyright: None,
1169 holder: None,
1170 declared_license_expression: None,
1171 declared_license_expression_spdx: None,
1172 license_detections: Vec::new(),
1173 other_license_expression: None,
1174 other_license_expression_spdx: None,
1175 other_license_detections: Vec::new(),
1176 extracted_license_statement: None,
1177 notice_text: None,
1178 source_packages: Vec::new(),
1179 file_references: Vec::new(),
1180 is_private: false,
1181 is_virtual: false,
1182 extra_data,
1183 dependencies,
1184 repository_homepage_url: None,
1185 repository_download_url: None,
1186 api_data_url: None,
1187 datasource_id: Some(DatasourceId::Godeps),
1188 purl,
1189 }
1190}