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