Skip to main content

provenant/parsers/
go.rs

1//! Parser for Go ecosystem dependency files.
2//!
3//! Extracts package metadata and dependencies from Go module management files
4//! and legacy dependency tracking formats.
5//!
6//! # Supported Formats
7//! - go.mod (Go module manifest with dependencies and version constraints)
8//! - go.sum (Go module checksum database for verification)
9//! - Godeps.json (Legacy dependency format from godep tool)
10//!
11//! # Key Features
12//! - go.mod dependency extraction with version constraint parsing
13//! - Direct vs transitive dependency tracking from require/indirect fields
14//! - Checksum extraction from go.sum for integrity verification
15//! - Legacy Godeps.json support for older projects
16//! - Package URL (purl) generation for golang packages
17//! - Module path parsing and namespace detection
18//!
19//! # Implementation Notes
20//! - PURL type: "golang"
21//! - All dependencies are pinned in go.mod/go.sum (`is_pinned: Some(true)`)
22//! - Graceful error handling with `warn!()` logs
23//! - Supports Go 1.11+ module syntax
24
25use 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
36/// Go go.mod manifest parser.
37///
38/// Extracts module declaration, require dependencies (with indirect marker
39/// preservation), and exclude directives from go.mod files.
40pub 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        // Bug #5: Reset block state on closing paren
90        if trimmed == ")" {
91            block_state = BlockState::None;
92            continue;
93        }
94
95        // Inside a block: dispatch by block type
96        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        // Block openings
122        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        // Module declaration
140        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        // Go version directive
151        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        // Toolchain directive
160        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        // Single-line require
169        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        // Single-line exclude
177        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        // Single-line replace (without opening paren)
185        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        // Single-line retract
196        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
296/// Parses a single dependency line from a require or exclude block/directive.
297///
298/// Handles:
299/// - Bug #2: Preserves `// indirect` marker as `is_direct = false`
300/// - Bug #8: `+incompatible` suffix in versions
301/// - Bug #10: Pseudo-versions (v0.0.0-YYYYMMDDHHMMSS-hash)
302///
303/// Format: `github.com/foo/bar v1.2.3 // indirect`
304fn 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    // Bug #2: Check for // indirect BEFORE stripping comments
311    let is_indirect = trimmed.contains("// indirect");
312    let is_direct = !is_indirect;
313
314    // Strip comment for parsing the module path and version
315    let without_comment = strip_comment(trimmed);
316    let without_comment = without_comment.trim();
317
318    // Split into module path and version
319    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
342/// Parses a replace line: `old-module [version] => new-module [version]`
343///
344/// Returns a `Dependency` with scope "replace" and extra_data containing
345/// replace_old, replace_new, replace_version, and optionally replace_old_version.
346fn 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
403/// Parses a retract value which can be a single version or a range `[v1, v2]`.
404fn 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
436/// Strips inline comments (everything after `//`) from a line.
437///
438/// Preserves the content before the comment marker.
439fn strip_comment(line: &str) -> &str {
440    match line.find("//") {
441        Some(idx) => &line[..idx],
442        None => line,
443    }
444}
445
446/// Creates a PURL for a Go module.
447///
448/// Format: `pkg:golang/namespace/name@version`
449/// The module path is split into namespace and name for PURL construction.
450pub(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
487/// Returns a default empty PackageData for Go modules.
488fn 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
532// ============================================================================
533// GoSumParser
534// ============================================================================
535
536pub 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
1030// ============================================================================
1031// GodepsParser
1032// ============================================================================
1033
1034pub 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);