Skip to main content

provenant/parsers/
go.rs

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