Skip to content

Commit d617687

Browse files
authored
Merge pull request #477 from open-edge-platform/fix/url-encode-plus-in-downloads
fix(pkgfetcher): encode + as %2B in download URLs
2 parents 0a01d50 + 16b0cd6 commit d617687

File tree

2 files changed

+60
-1
lines changed

2 files changed

+60
-1
lines changed

internal/ospackage/pkgfetcher/pkgfetcher.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"path"
99
"path/filepath"
10+
"strings"
1011
"sync"
1112
"sync/atomic"
1213
"time"
@@ -175,7 +176,10 @@ func FetchPackages(urls []string, destDir string, workers int) error {
175176
log.Warnf("re-downloading zero-size %s", name)
176177
}
177178
client := network.GetSecureHTTPClient()
178-
err := downloadWithRetry(client, url, destPath, i)
179+
// S3/CloudFront treats literal '+' as space; encode it as %2B in the
180+
// download URL only (the local filename keeps the original '+').
181+
downloadURL := strings.ReplaceAll(url, "+", "%2B")
182+
err := downloadWithRetry(client, downloadURL, destPath, i)
179183

180184
if err != nil {
181185
log.Errorf("downloading %s failed: %v", url, err)

internal/ospackage/pkgfetcher/pkgfetcher_test.go

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -609,3 +609,58 @@ func TestDownloadWithRetry_RemovaPartialFileOnError(t *testing.T) {
609609
t.Fatalf("expected partial file to be removed, statErr=%v", statErr)
610610
}
611611
}
612+
613+
// TestFetchPackages_PlusEncodedAsPercentTwoBInURL verifies that FetchPackages
614+
// encodes '+' as '%2B' in the HTTP request URL (S3/CloudFront treats literal
615+
// '+' as space), while the destination filename retains the original '+'.
616+
func TestFetchPackages_PlusEncodedAsPercentTwoBInURL(t *testing.T) {
617+
tempDir, err := os.MkdirTemp("", "pkgfetcher_test")
618+
if err != nil {
619+
t.Fatalf("Failed to create temp dir: %v", err)
620+
}
621+
defer os.RemoveAll(tempDir)
622+
623+
var receivedPath string
624+
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
625+
receivedPath = r.URL.RawPath
626+
if receivedPath == "" {
627+
// RawPath is empty when the path has no encoded characters after Go
628+
// normalisation, so fall back to RequestURI which preserves encoding.
629+
receivedPath = r.RequestURI
630+
}
631+
w.WriteHeader(http.StatusOK)
632+
_, _ = w.Write([]byte("eci-package-content"))
633+
}))
634+
defer server.Close()
635+
636+
// Simulate an ECI package filename containing '+' characters.
637+
filename := "systemd_255.4-1ubuntu8.12-ecir8+etf+taprio_amd64.deb"
638+
inputURL := server.URL + "/pool/main/s/systemd/" + filename
639+
640+
err = FetchPackages([]string{inputURL}, tempDir, 1)
641+
if err != nil {
642+
t.Fatalf("FetchPackages failed: %v", err)
643+
}
644+
645+
// The server must have received '%2B' instead of '+'.
646+
if !strings.Contains(receivedPath, "%2B") {
647+
t.Errorf("server received path with literal '+' instead of '%%2B': %s", receivedPath)
648+
}
649+
if strings.Contains(receivedPath, "+") {
650+
t.Errorf("server path still contains literal '+': %s", receivedPath)
651+
}
652+
653+
// The destination file must retain the original '+' in its name.
654+
destPath := filepath.Join(tempDir, filename)
655+
if _, statErr := os.Stat(destPath); os.IsNotExist(statErr) {
656+
t.Fatalf("expected destination file with '+' in name to exist: %s", destPath)
657+
}
658+
659+
content, readErr := os.ReadFile(destPath)
660+
if readErr != nil {
661+
t.Fatalf("failed to read downloaded file: %v", readErr)
662+
}
663+
if string(content) != "eci-package-content" {
664+
t.Fatalf("unexpected file content: %q", string(content))
665+
}
666+
}

0 commit comments

Comments
 (0)