Skip to main content

provenant/parsers/
go.rs

1// SPDX-FileCopyrightText: Provenant contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! Parser for Go ecosystem dependency files.
5//!
6//! Extracts package metadata and dependencies from Go module management files
7//! and legacy dependency tracking formats.
8//!
9//! # Supported Formats
10//! - go.mod (Go module manifest with dependencies and version constraints)
11//! - go.sum (Go module checksum database for verification)
12//! - Godeps.json (Legacy dependency format from godep tool)
13//!
14//! # Key Features
15//! - go.mod dependency extraction with version constraint parsing
16//! - Direct vs transitive dependency tracking from require/indirect fields
17//! - Checksum extraction from go.sum for integrity verification
18//! - Legacy Godeps.json support for older projects
19//! - Package URL (purl) generation for golang packages
20//! - Module path parsing and namespace detection
21//!
22//! # Implementation Notes
23//! - PURL type: "golang"
24//! - All dependencies are pinned in go.mod/go.sum (`is_pinned: Some(true)`)
25//! - Graceful error handling with `warn!()` logs
26//! - Supports Go 1.11+ module syntax
27
28use 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
39/// Go go.mod manifest parser.
40///
41/// Extracts module declaration, require dependencies (with indirect marker
42/// preservation), and exclude directives from go.mod files.
43pub 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        // Bug #5: Reset block state on closing paren
93        if trimmed == ")" {
94            block_state = BlockState::None;
95            continue;
96        }
97
98        // Inside a block: dispatch by block type
99        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        // Block openings
125        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        // Module declaration
143        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        // Go version directive
154        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        // Toolchain directive
163        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        // Single-line require
172        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        // Single-line exclude
180        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        // Single-line replace (without opening paren)
188        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        // Single-line retract
199        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
299/// Parses a single dependency line from a require or exclude block/directive.
300///
301/// Handles:
302/// - Bug #2: Preserves `// indirect` marker as `is_direct = false`
303/// - Bug #8: `+incompatible` suffix in versions
304/// - Bug #10: Pseudo-versions (v0.0.0-YYYYMMDDHHMMSS-hash)
305///
306/// Format: `github.com/foo/bar v1.2.3 // indirect`
307fn 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    // Bug #2: Check for // indirect BEFORE stripping comments
314    let is_indirect = trimmed.contains("// indirect");
315    let is_direct = !is_indirect;
316
317    // Strip comment for parsing the module path and version
318    let without_comment = strip_comment(trimmed);
319    let without_comment = without_comment.trim();
320
321    // Split into module path and version
322    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
345/// Parses a replace line: `old-module [version] => new-module [version]`
346///
347/// Returns a `Dependency` with scope "replace" and extra_data containing
348/// replace_old, replace_new, replace_version, and optionally replace_old_version.
349fn 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
406/// Parses a retract value which can be a single version or a range `[v1, v2]`.
407fn 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
439/// Strips inline comments (everything after `//`) from a line.
440///
441/// Preserves the content before the comment marker.
442fn strip_comment(line: &str) -> &str {
443    match line.find("//") {
444        Some(idx) => &line[..idx],
445        None => line,
446    }
447}
448
449/// Creates a PURL for a Go module.
450///
451/// Format: `pkg:golang/namespace/name@version`
452/// The module path is split into namespace and name for PURL construction.
453pub(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
490/// Returns a default empty PackageData for Go modules.
491fn 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
535// ============================================================================
536// GoSumParser
537// ============================================================================
538
539pub 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
1033// ============================================================================
1034// GodepsParser
1035// ============================================================================
1036
1037pub 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);