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