Skip to content

Commit b68184c

Browse files
committed
Fix composer dist URL rewriting and browse source for extensionless filenames
GitHub zipball URLs end in a bare commit hash with no file extension. rewriteDistURL now appends .zip when the filename has no extension and the dist type is zip. expandMinifiedVersions deep copies inherited values so in-place URL rewriting no longer corrupts shared references. browse.go infers .zip for extensionless filenames so existing cached artifacts can still be opened.
1 parent bcbb883 commit b68184c

File tree

2 files changed

+45
-5
lines changed

2 files changed

+45
-5
lines changed

internal/handler/composer.go

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,10 @@ func expandMinifiedVersions(versionList []any) []any {
182182
}
183183

184184
// Merge inherited fields into a new map, then overlay current fields.
185+
// Deep copy values to avoid shared references between versions.
185186
merged := make(map[string]any, len(inherited)+len(vmap))
186187
for k, val := range inherited {
187-
merged[k] = val
188+
merged[k] = deepCopyValue(val)
188189
}
189190
for k, val := range vmap {
190191
merged[k] = val
@@ -199,6 +200,26 @@ func expandMinifiedVersions(versionList []any) []any {
199200
return expanded
200201
}
201202

203+
// deepCopyValue returns a deep copy of JSON-like values (maps, slices, scalars).
204+
func deepCopyValue(v any) any {
205+
switch val := v.(type) {
206+
case map[string]any:
207+
m := make(map[string]any, len(val))
208+
for k, v := range val {
209+
m[k] = deepCopyValue(v)
210+
}
211+
return m
212+
case []any:
213+
s := make([]any, len(val))
214+
for i, v := range val {
215+
s[i] = deepCopyValue(v)
216+
}
217+
return s
218+
default:
219+
return v
220+
}
221+
}
222+
202223
// filterAndRewriteVersions applies cooldown filtering and rewrites dist URLs
203224
// for a single package's version list.
204225
func (h *ComposerHandler) filterAndRewriteVersions(packageName string, versionList []any) []any {
@@ -266,6 +287,14 @@ func (h *ComposerHandler) rewriteDistURL(vmap map[string]any, packageName, versi
266287
filename = url[idx+1:]
267288
}
268289

290+
// GitHub zipball URLs end with a bare commit hash (no extension).
291+
// Append .zip so the archives library can detect the format.
292+
if !strings.Contains(filename, ".") {
293+
if distType, _ := dist["type"].(string); distType == "zip" {
294+
filename += ".zip"
295+
}
296+
}
297+
269298
parts := strings.SplitN(packageName, "/", vendorPackageParts)
270299
if len(parts) == vendorPackageParts {
271300
newURL := fmt.Sprintf("%s/composer/files/%s/%s/%s/%s",

internal/server/browse.go

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,17 @@ import (
1717

1818
const contentTypePlainText = "text/plain; charset=utf-8"
1919

20+
// archiveFilename returns a filename suitable for archive format detection.
21+
// Some ecosystems (e.g. composer) store artifacts with bare hash filenames
22+
// that have no extension. This adds .zip when the original has no extension
23+
// and the content is likely a zip archive.
24+
func archiveFilename(filename string) string {
25+
if !strings.Contains(filename, ".") {
26+
return filename + ".zip"
27+
}
28+
return filename
29+
}
30+
2031
// getStripPrefix returns the path prefix to strip for a given ecosystem.
2132
// npm packages wrap content in a "package/" directory.
2233
func getStripPrefix(ecosystem string) string {
@@ -176,7 +187,7 @@ func (s *Server) browseList(w http.ResponseWriter, r *http.Request, ecosystem, n
176187

177188
// Open archive with appropriate prefix stripping
178189
stripPrefix := getStripPrefix(ecosystem)
179-
archiveReader, err := archives.OpenWithPrefix(cachedArtifact.Filename, artifactReader, stripPrefix)
190+
archiveReader, err := archives.OpenWithPrefix(archiveFilename(cachedArtifact.Filename), artifactReader, stripPrefix)
180191
if err != nil {
181192
s.logger.Error("failed to open archive", "error", err, "filename", cachedArtifact.Filename)
182193
http.Error(w, "failed to open archive", http.StatusInternalServerError)
@@ -271,7 +282,7 @@ func (s *Server) browseFile(w http.ResponseWriter, r *http.Request, ecosystem, n
271282

272283
// Open archive with appropriate prefix stripping
273284
stripPrefix := getStripPrefix(ecosystem)
274-
archiveReader, err := archives.OpenWithPrefix(cachedArtifact.Filename, artifactReader, stripPrefix)
285+
archiveReader, err := archives.OpenWithPrefix(archiveFilename(cachedArtifact.Filename), artifactReader, stripPrefix)
275286
if err != nil {
276287
s.logger.Error("failed to open archive", "error", err, "filename", cachedArtifact.Filename)
277288
http.Error(w, "failed to open archive", http.StatusInternalServerError)
@@ -486,15 +497,15 @@ func (s *Server) compareDiff(w http.ResponseWriter, r *http.Request, ecosystem,
486497

487498
stripPrefix := getStripPrefix(ecosystem)
488499

489-
fromArchive, err := archives.OpenWithPrefix(fromArtifact.Filename, fromReader, stripPrefix)
500+
fromArchive, err := archives.OpenWithPrefix(archiveFilename(fromArtifact.Filename), fromReader, stripPrefix)
490501
if err != nil {
491502
s.logger.Error("failed to open from archive", "error", err)
492503
http.Error(w, "failed to open from archive", http.StatusInternalServerError)
493504
return
494505
}
495506
defer func() { _ = fromArchive.Close() }()
496507

497-
toArchive, err := archives.OpenWithPrefix(toArtifact.Filename, toReader, stripPrefix)
508+
toArchive, err := archives.OpenWithPrefix(archiveFilename(toArtifact.Filename), toReader, stripPrefix)
498509
if err != nil {
499510
s.logger.Error("failed to open to archive", "error", err)
500511
http.Error(w, "failed to open to archive", http.StatusInternalServerError)

0 commit comments

Comments
 (0)