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(ns) = &namespace
492        && let Err(e) = purl.with_namespace(ns)
493    {
494        warn!(
495            "Failed to set namespace '{}' for golang module '{}': {}",
496            ns, module_path, e
497        );
498        return None;
499    }
500
501    if let Some(v) = version
502        && let Err(e) = purl.with_version(v)
503    {
504        warn!(
505            "Failed to set version '{}' for golang module '{}': {}",
506            v, module_path, e
507        );
508        return None;
509    }
510
511    Some(purl.to_string())
512}
513
514/// Returns a default empty PackageData for Go modules.
515fn default_package_data() -> PackageData {
516    PackageData {
517        package_type: Some(PACKAGE_TYPE),
518        primary_language: Some("Go".to_string()),
519        ..Default::default()
520    }
521}
522
523fn default_go_mod_package_data() -> PackageData {
524    PackageData {
525        datasource_id: Some(DatasourceId::GoMod),
526        ..default_package_data()
527    }
528}
529
530fn default_go_sum_package_data() -> PackageData {
531    PackageData {
532        datasource_id: Some(DatasourceId::GoSum),
533        ..default_package_data()
534    }
535}
536
537fn default_go_work_package_data() -> PackageData {
538    PackageData {
539        datasource_id: Some(DatasourceId::GoWork),
540        ..default_package_data()
541    }
542}
543
544fn default_godeps_package_data() -> PackageData {
545    PackageData {
546        datasource_id: Some(DatasourceId::Godeps),
547        ..default_package_data()
548    }
549}
550
551// ============================================================================
552// GoSumParser
553// ============================================================================
554
555pub struct GoSumParser;
556
557impl PackageParser for GoSumParser {
558    const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
559
560    fn metadata() -> Vec<ParserMetadata> {
561        vec![ParserMetadata {
562            description: "Go go.sum checksum database",
563            file_patterns: &["**/go.sum"],
564            package_type: "golang",
565            primary_language: "Go",
566            documentation_url: Some("https://go.dev/ref/mod#go-sum-files"),
567        }]
568    }
569
570    fn extract_packages(path: &Path) -> Vec<PackageData> {
571        let content = match read_file_to_string(path, None) {
572            Ok(c) => c,
573            Err(e) => {
574                warn!("Failed to read go.sum at {:?}: {}", path, e);
575                return vec![default_go_sum_package_data()];
576            }
577        };
578
579        vec![parse_go_sum(&content)]
580    }
581
582    fn is_match(path: &Path) -> bool {
583        path.file_name().is_some_and(|name| name == "go.sum")
584    }
585}
586
587pub fn parse_go_sum(content: &str) -> PackageData {
588    let mut dependencies = Vec::new();
589    let mut seen = HashSet::new();
590
591    for line in content.lines().take(MAX_ITERATION_COUNT) {
592        let trimmed = line.trim();
593        if trimmed.is_empty() {
594            continue;
595        }
596
597        let parts: Vec<&str> = trimmed.split_whitespace().collect();
598        if parts.len() < 3 || !parts[2].starts_with("h1:") {
599            continue;
600        }
601
602        let module = parts[0];
603        let raw_version = parts[1];
604
605        let version = raw_version.strip_suffix("/go.mod").unwrap_or(raw_version);
606
607        let key = format!("{}@{}", module, version);
608        if seen.contains(&key) {
609            continue;
610        }
611        seen.insert(key);
612
613        let purl = create_golang_purl(module, Some(version));
614
615        dependencies.push(Dependency {
616            purl,
617            extracted_requirement: Some(truncate_field(version.to_string())),
618            scope: Some("dependency".to_string()),
619            is_runtime: Some(true),
620            is_optional: Some(false),
621            is_pinned: Some(true),
622            is_direct: None,
623            resolved_package: None,
624            extra_data: None,
625        });
626    }
627
628    PackageData {
629        package_type: Some(PACKAGE_TYPE),
630        namespace: None,
631        name: None,
632        version: None,
633        qualifiers: None,
634        subpath: None,
635        primary_language: Some("Go".to_string()),
636        description: None,
637        release_date: None,
638        parties: Vec::new(),
639        keywords: Vec::new(),
640        homepage_url: None,
641        download_url: None,
642        size: None,
643        sha1: None,
644        md5: None,
645        sha256: None,
646        sha512: None,
647        bug_tracking_url: None,
648        code_view_url: None,
649        vcs_url: None,
650        copyright: None,
651        holder: None,
652        declared_license_expression: None,
653        declared_license_expression_spdx: None,
654        license_detections: Vec::new(),
655        other_license_expression: None,
656        other_license_expression_spdx: None,
657        other_license_detections: Vec::new(),
658        extracted_license_statement: None,
659        notice_text: None,
660        source_packages: Vec::new(),
661        file_references: Vec::new(),
662        is_private: false,
663        is_virtual: false,
664        extra_data: None,
665        dependencies,
666        repository_homepage_url: None,
667        repository_download_url: None,
668        api_data_url: None,
669        datasource_id: Some(DatasourceId::GoSum),
670        purl: None,
671    }
672}
673
674pub struct GoWorkParser;
675
676impl PackageParser for GoWorkParser {
677    const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
678
679    fn metadata() -> Vec<ParserMetadata> {
680        vec![ParserMetadata {
681            description: "Go go.work workspace file",
682            file_patterns: &["**/go.work"],
683            package_type: "golang",
684            primary_language: "Go",
685            documentation_url: Some("https://go.dev/ref/mod#go-work-files"),
686        }]
687    }
688
689    fn extract_packages(path: &Path) -> Vec<PackageData> {
690        let content = match read_file_to_string(path, None) {
691            Ok(c) => c,
692            Err(e) => {
693                warn!("Failed to read go.work at {:?}: {}", path, e);
694                return vec![default_go_work_package_data()];
695            }
696        };
697
698        vec![parse_go_work(&content, path)]
699    }
700
701    fn is_match(path: &Path) -> bool {
702        path.file_name().is_some_and(|name| name == "go.work")
703    }
704}
705
706pub fn parse_go_work(content: &str, work_path: &Path) -> PackageData {
707    let mut go_version: Option<String> = None;
708    let mut toolchain: Option<String> = None;
709    let mut use_paths: Vec<String> = Vec::new();
710    let mut replace_deps: Vec<Dependency> = Vec::new();
711    let mut unresolved_use_paths: Vec<String> = Vec::new();
712    let mut block_state = BlockState::None;
713
714    for line in content.lines().take(MAX_ITERATION_COUNT) {
715        let trimmed = line.trim();
716
717        if trimmed.is_empty() || trimmed.starts_with("//") {
718            continue;
719        }
720
721        if trimmed == ")" {
722            block_state = BlockState::None;
723            continue;
724        }
725
726        if block_state != BlockState::None {
727            match block_state {
728                BlockState::Require => {
729                    let use_path = extract_single_go_token(trimmed);
730                    if let Some(use_path) = use_path.filter(|path| !path.is_empty()) {
731                        use_paths.push(truncate_field(use_path));
732                    }
733                }
734                BlockState::Replace => {
735                    if let Some(dep) = parse_workspace_replace_line(trimmed) {
736                        replace_deps.push(dep);
737                    }
738                }
739                _ => {}
740            }
741            continue;
742        }
743
744        if trimmed.starts_with("use") && trimmed.contains('(') {
745            block_state = BlockState::Require;
746            continue;
747        }
748        if trimmed.starts_with("replace") && trimmed.contains('(') {
749            block_state = BlockState::Replace;
750            continue;
751        }
752
753        if let Some(version) = trimmed.strip_prefix("go ") {
754            let version = strip_comment(version).trim();
755            if !version.is_empty() {
756                go_version = Some(truncate_field(version.to_string()));
757            }
758            continue;
759        }
760
761        if let Some(tc) = trimmed.strip_prefix("toolchain ") {
762            let tc = strip_comment(tc).trim();
763            if !tc.is_empty() {
764                toolchain = Some(truncate_field(tc.to_string()));
765            }
766            continue;
767        }
768
769        if let Some(rest) = trimmed.strip_prefix("use ") {
770            let use_path = extract_single_go_token(rest);
771            if let Some(use_path) = use_path.filter(|path| !path.is_empty()) {
772                use_paths.push(truncate_field(use_path));
773            }
774            continue;
775        }
776
777        if let Some(rest) = trimmed.strip_prefix("replace ") {
778            if let Some(dep) = parse_workspace_replace_line(rest) {
779                replace_deps.push(dep);
780            }
781            continue;
782        }
783    }
784
785    if go_version.is_none() || use_paths.is_empty() {
786        warn!("Invalid go.work: missing go directive or use directive");
787        return default_go_work_package_data();
788    }
789
790    let (mut dependencies, unresolved) = resolve_workspace_use_dependencies(work_path, &use_paths);
791    dependencies.extend(replace_deps);
792    unresolved_use_paths.extend(unresolved);
793
794    let mut extra_data = HashMap::new();
795    if let Some(v) = go_version {
796        extra_data.insert("go_version".to_string(), serde_json::Value::String(v));
797    }
798    if let Some(tc) = toolchain {
799        extra_data.insert("toolchain".to_string(), serde_json::Value::String(tc));
800    }
801    extra_data.insert(
802        "use_paths".to_string(),
803        serde_json::Value::Array(
804            use_paths
805                .iter()
806                .cloned()
807                .map(serde_json::Value::String)
808                .collect(),
809        ),
810    );
811    if !unresolved_use_paths.is_empty() {
812        extra_data.insert(
813            "unresolved_use_paths".to_string(),
814            serde_json::Value::Array(
815                unresolved_use_paths
816                    .into_iter()
817                    .map(serde_json::Value::String)
818                    .collect(),
819            ),
820        );
821    }
822
823    PackageData {
824        package_type: Some(PACKAGE_TYPE),
825        namespace: None,
826        name: None,
827        version: None,
828        qualifiers: None,
829        subpath: None,
830        primary_language: Some("Go".to_string()),
831        description: None,
832        release_date: None,
833        parties: Vec::new(),
834        keywords: Vec::new(),
835        homepage_url: None,
836        download_url: None,
837        size: None,
838        sha1: None,
839        md5: None,
840        sha256: None,
841        sha512: None,
842        bug_tracking_url: None,
843        code_view_url: None,
844        vcs_url: None,
845        copyright: None,
846        holder: None,
847        declared_license_expression: None,
848        declared_license_expression_spdx: None,
849        license_detections: Vec::new(),
850        other_license_expression: None,
851        other_license_expression_spdx: None,
852        other_license_detections: Vec::new(),
853        extracted_license_statement: None,
854        notice_text: None,
855        source_packages: Vec::new(),
856        file_references: Vec::new(),
857        is_private: false,
858        is_virtual: false,
859        extra_data: Some(extra_data),
860        dependencies,
861        repository_homepage_url: None,
862        repository_download_url: None,
863        api_data_url: None,
864        datasource_id: Some(DatasourceId::GoWork),
865        purl: None,
866    }
867}
868
869fn resolve_workspace_use_dependencies(
870    work_path: &Path,
871    use_paths: &[String],
872) -> (Vec<Dependency>, Vec<String>) {
873    let Some(base_dir) = work_path.parent() else {
874        return (Vec::new(), use_paths.to_vec());
875    };
876
877    let mut dependencies = Vec::new();
878    let mut unresolved = Vec::new();
879
880    for use_path in use_paths.iter().take(MAX_ITERATION_COUNT) {
881        let go_mod_path = base_dir.join(use_path).join("go.mod");
882        let module_path = read_file_to_string(&go_mod_path, None)
883            .ok()
884            .and_then(|content| extract_module_path_from_go_mod(&content));
885
886        let purl = module_path
887            .as_deref()
888            .and_then(|module_path| create_golang_purl(module_path, None));
889
890        if purl.is_none() {
891            unresolved.push(use_path.clone());
892            continue;
893        }
894
895        let mut extra_data = HashMap::new();
896        extra_data.insert(
897            "workspace_path".to_string(),
898            serde_json::Value::String(truncate_field(use_path.clone())),
899        );
900        if let Some(module_path) = module_path {
901            extra_data.insert(
902                "workspace_module_path".to_string(),
903                serde_json::Value::String(truncate_field(module_path)),
904            );
905        }
906
907        dependencies.push(Dependency {
908            purl,
909            extracted_requirement: Some(truncate_field(use_path.clone())),
910            scope: Some("use".to_string()),
911            is_runtime: Some(true),
912            is_optional: Some(false),
913            is_pinned: Some(false),
914            is_direct: Some(true),
915            resolved_package: None,
916            extra_data: Some(extra_data),
917        });
918    }
919
920    (dependencies, unresolved)
921}
922
923fn extract_module_path_from_go_mod(content: &str) -> Option<String> {
924    for line in content.lines().take(MAX_ITERATION_COUNT) {
925        let trimmed = line.trim();
926        if let Some(module_path) = trimmed.strip_prefix("module ") {
927            let module_path = strip_comment(module_path).trim();
928            if !module_path.is_empty() {
929                return Some(truncate_field(module_path.to_string()));
930            }
931        }
932    }
933    None
934}
935
936fn parse_workspace_replace_line(line: &str) -> Option<Dependency> {
937    let line = strip_comment(line).trim();
938    let parts: Vec<&str> = line.splitn(2, "=>").collect();
939    if parts.len() != 2 {
940        return None;
941    }
942
943    let old_parts = parse_go_tokens(parts[0]);
944    let new_parts = parse_go_tokens(parts[1]);
945    if old_parts.is_empty() || new_parts.is_empty() {
946        return None;
947    }
948
949    let old_module = old_parts[0].as_str();
950    let old_version = old_parts.get(1).map(|s| s.as_str());
951    let new_module = new_parts[0].as_str();
952    let new_version = new_parts.get(1).map(|s| truncate_field(s.clone()));
953    let is_local_path = is_local_go_replace_path(new_module);
954
955    let purl = if is_local_path {
956        None
957    } else {
958        create_golang_purl(new_module, new_version.as_deref())
959    };
960
961    let mut extra = std::collections::HashMap::new();
962    extra.insert(
963        "replace_old".to_string(),
964        serde_json::Value::String(truncate_field(old_module.to_string())),
965    );
966    extra.insert(
967        "replace_new".to_string(),
968        serde_json::Value::String(truncate_field(new_module.to_string())),
969    );
970    if let Some(ref v) = new_version {
971        extra.insert(
972            "replace_version".to_string(),
973            serde_json::Value::String(v.clone()),
974        );
975    }
976    if let Some(ov) = old_version {
977        extra.insert(
978            "replace_old_version".to_string(),
979            serde_json::Value::String(truncate_field(ov.to_string())),
980        );
981    }
982    if is_local_path {
983        extra.insert(
984            "replace_local_path".to_string(),
985            serde_json::Value::Bool(true),
986        );
987    }
988
989    Some(Dependency {
990        purl,
991        extracted_requirement: new_version,
992        scope: Some("replace".to_string()),
993        is_runtime: Some(true),
994        is_optional: Some(false),
995        is_pinned: Some(false),
996        is_direct: Some(true),
997        resolved_package: None,
998        extra_data: Some(extra),
999    })
1000}
1001
1002fn is_local_go_replace_path(module: &str) -> bool {
1003    module.starts_with("./")
1004        || module.starts_with("../")
1005        || module.starts_with('/')
1006        || module.starts_with('~')
1007}
1008
1009fn extract_single_go_token(value: &str) -> Option<String> {
1010    parse_go_tokens(value).into_iter().next()
1011}
1012
1013fn parse_go_tokens(value: &str) -> Vec<String> {
1014    let mut tokens = Vec::new();
1015    let mut current = String::new();
1016    let mut quote: Option<char> = None;
1017    let mut chars = value.chars().peekable();
1018
1019    while let Some(ch) = chars.next() {
1020        if let Some(active_quote) = quote {
1021            if ch == active_quote {
1022                quote = None;
1023                continue;
1024            }
1025
1026            if active_quote == '"' && ch == '\\' {
1027                if let Some(next) = chars.next() {
1028                    current.push(next);
1029                }
1030                continue;
1031            }
1032
1033            current.push(ch);
1034            continue;
1035        }
1036
1037        match ch {
1038            '"' | '`' => {
1039                quote = Some(ch);
1040            }
1041            c if c.is_whitespace() => {
1042                if !current.is_empty() {
1043                    tokens.push(std::mem::take(&mut current));
1044                }
1045            }
1046            _ => current.push(ch),
1047        }
1048    }
1049
1050    if !current.is_empty() {
1051        tokens.push(current);
1052    }
1053
1054    tokens
1055}
1056
1057// ============================================================================
1058// GodepsParser
1059// ============================================================================
1060
1061pub struct GodepsParser;
1062
1063impl PackageParser for GodepsParser {
1064    const PACKAGE_TYPE: PackageType = PACKAGE_TYPE;
1065
1066    fn metadata() -> Vec<ParserMetadata> {
1067        vec![ParserMetadata {
1068            description: "Go Godeps.json legacy dependency file",
1069            file_patterns: &["**/Godeps.json"],
1070            package_type: "golang",
1071            primary_language: "Go",
1072            documentation_url: None,
1073        }]
1074    }
1075
1076    fn extract_packages(path: &Path) -> Vec<PackageData> {
1077        let content = match read_file_to_string(path, None) {
1078            Ok(c) => c,
1079            Err(e) => {
1080                warn!("Failed to read Godeps.json at {:?}: {}", path, e);
1081                return vec![default_godeps_package_data()];
1082            }
1083        };
1084
1085        vec![parse_godeps_json(&content)]
1086    }
1087
1088    fn is_match(path: &Path) -> bool {
1089        path.file_name().is_some_and(|name| name == "Godeps.json")
1090    }
1091}
1092
1093pub fn parse_godeps_json(content: &str) -> PackageData {
1094    let json: serde_json::Value = match serde_json::from_str(content) {
1095        Ok(j) => j,
1096        Err(e) => {
1097            warn!("Failed to parse Godeps.json: {}", e);
1098            return default_godeps_package_data();
1099        }
1100    };
1101
1102    let import_path = json
1103        .get("ImportPath")
1104        .and_then(|v| v.as_str())
1105        .map(|s| truncate_field(s.to_string()));
1106
1107    let go_version = json
1108        .get("GoVersion")
1109        .and_then(|v| v.as_str())
1110        .map(|s| truncate_field(s.to_string()));
1111
1112    let (namespace, name) = match &import_path {
1113        Some(ip) => {
1114            let (ns, n) = split_module_path(ip);
1115            (ns, Some(n))
1116        }
1117        None => (None, None),
1118    };
1119
1120    let purl = import_path
1121        .as_deref()
1122        .and_then(|ip| create_golang_purl(ip, None));
1123
1124    let mut dependencies = Vec::new();
1125
1126    if let Some(deps) = json.get("Deps").and_then(|v| v.as_array()) {
1127        for dep in deps.iter().take(MAX_ITERATION_COUNT) {
1128            let dep_import_path = dep.get("ImportPath").and_then(|v| v.as_str());
1129            let rev = dep.get("Rev").and_then(|v| v.as_str());
1130
1131            if let Some(path) = dep_import_path {
1132                let dep_purl = create_golang_purl(path, None);
1133
1134                dependencies.push(Dependency {
1135                    purl: dep_purl,
1136                    extracted_requirement: rev.map(|s| truncate_field(s.to_string())),
1137                    scope: Some("Deps".to_string()),
1138                    is_runtime: Some(true),
1139                    is_optional: Some(false),
1140                    is_pinned: Some(false),
1141                    is_direct: None,
1142                    resolved_package: None,
1143                    extra_data: None,
1144                });
1145            }
1146        }
1147    }
1148
1149    let extra_data = go_version.map(|v| {
1150        let mut map = HashMap::new();
1151        map.insert("go_version".to_string(), serde_json::Value::String(v));
1152        map
1153    });
1154
1155    let homepage_url = import_path
1156        .as_ref()
1157        .map(|m| truncate_field(format!("https://pkg.go.dev/{}", m)));
1158
1159    let vcs_url = import_path
1160        .as_ref()
1161        .map(|m| truncate_field(format!("https://{}.git", m)));
1162
1163    PackageData {
1164        package_type: Some(PACKAGE_TYPE),
1165        namespace,
1166        name,
1167        version: None,
1168        qualifiers: None,
1169        subpath: None,
1170        primary_language: Some("Go".to_string()),
1171        description: None,
1172        release_date: None,
1173        parties: Vec::new(),
1174        keywords: Vec::new(),
1175        homepage_url,
1176        download_url: None,
1177        size: None,
1178        sha1: None,
1179        md5: None,
1180        sha256: None,
1181        sha512: None,
1182        bug_tracking_url: None,
1183        code_view_url: None,
1184        vcs_url,
1185        copyright: None,
1186        holder: None,
1187        declared_license_expression: None,
1188        declared_license_expression_spdx: None,
1189        license_detections: Vec::new(),
1190        other_license_expression: None,
1191        other_license_expression_spdx: None,
1192        other_license_detections: Vec::new(),
1193        extracted_license_statement: None,
1194        notice_text: None,
1195        source_packages: Vec::new(),
1196        file_references: Vec::new(),
1197        is_private: false,
1198        is_virtual: false,
1199        extra_data,
1200        dependencies,
1201        repository_homepage_url: None,
1202        repository_download_url: None,
1203        api_data_url: None,
1204        datasource_id: Some(DatasourceId::Godeps),
1205        purl,
1206    }
1207}