diff --git a/pkg/cli/download_workflow.go b/pkg/cli/download_workflow.go index 087e15badc..2f8bba1c7a 100644 --- a/pkg/cli/download_workflow.go +++ b/pkg/cli/download_workflow.go @@ -1,6 +1,7 @@ package cli import ( + "encoding/base64" "fmt" "os" "os/exec" @@ -172,13 +173,17 @@ func downloadWorkflowContent(repo, path, ref string, verbose bool) ([]byte, erro } // The content is base64 encoded, decode it - contentBase64 := strings.TrimSpace(string(output)) - base64Cmd := exec.Command("base64", "-d") - base64Cmd.Stdin = strings.NewReader(contentBase64) - content, err := base64Cmd.Output() + return decodeBase64FileContent(string(output)) +} + +// decodeBase64FileContent decodes base64-encoded file content returned by the GitHub API. +// The GitHub API wraps lines at 60 characters and may include surrounding whitespace, +// so both are stripped before decoding. +func decodeBase64FileContent(raw string) ([]byte, error) { + cleaned := strings.ReplaceAll(strings.TrimSpace(raw), "\n", "") + content, err := base64.StdEncoding.DecodeString(cleaned) if err != nil { return nil, fmt.Errorf("failed to decode file content: %w", err) } - return content, nil } diff --git a/pkg/cli/download_workflow_test.go b/pkg/cli/download_workflow_test.go new file mode 100644 index 0000000000..b6309fd631 --- /dev/null +++ b/pkg/cli/download_workflow_test.go @@ -0,0 +1,77 @@ +//go:build !integration + +package cli + +import ( + "encoding/base64" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDecodeBase64FileContent(t *testing.T) { + tests := []struct { + name string + input func() string // build the raw API-style input + expected string + wantErr bool + }{ + { + name: "plain base64 without newlines", + input: func() string { + return base64.StdEncoding.EncodeToString([]byte("hello world")) + }, + expected: "hello world", + }, + { + name: "GitHub API style with embedded newlines every 60 chars", + input: func() string { + encoded := base64.StdEncoding.EncodeToString([]byte("hello world")) + // Simulate GitHub API line-wrapping at 60 characters + var sb strings.Builder + for i, c := range encoded { + if i > 0 && i%60 == 0 { + sb.WriteByte('\n') + } + sb.WriteRune(c) + } + return sb.String() + }, + expected: "hello world", + }, + { + name: "leading and trailing whitespace stripped", + input: func() string { + return " " + base64.StdEncoding.EncodeToString([]byte("trim me")) + "\n" + }, + expected: "trim me", + }, + { + name: "binary content round-trips correctly", + input: func() string { + data := []byte{0x00, 0x01, 0x02, 0xFF, 0xFE} + return base64.StdEncoding.EncodeToString(data) + }, + expected: string([]byte{0x00, 0x01, 0x02, 0xFF, 0xFE}), + }, + { + name: "invalid base64 returns error", + input: func() string { return "!!!not-valid-base64!!!" }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := decodeBase64FileContent(tt.input()) + if tt.wantErr { + assert.Error(t, err, "expected an error for invalid base64 input") + return + } + require.NoError(t, err, "unexpected error decoding base64 content") + assert.Equal(t, tt.expected, string(got), "decoded content should match expected") + }) + } +}