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