mirror of
https://github.com/git-pkgs/proxy.git
synced 2026-08-01 01:58:09 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36f3a51c65 |
5 changed files with 192 additions and 112 deletions
3
go.mod
3
go.mod
|
|
@ -5,9 +5,10 @@ go 1.25.6
|
||||||
require (
|
require (
|
||||||
github.com/BurntSushi/toml v1.6.0
|
github.com/BurntSushi/toml v1.6.0
|
||||||
github.com/CycloneDX/cyclonedx-go v0.11.0
|
github.com/CycloneDX/cyclonedx-go v0.11.0
|
||||||
github.com/git-pkgs/archives v0.3.1
|
github.com/git-pkgs/archives v0.4.0
|
||||||
github.com/git-pkgs/cooldown v0.1.1
|
github.com/git-pkgs/cooldown v0.1.1
|
||||||
github.com/git-pkgs/enrichment v0.6.4
|
github.com/git-pkgs/enrichment v0.6.4
|
||||||
|
github.com/git-pkgs/magic v0.1.0
|
||||||
github.com/git-pkgs/purl v0.1.15
|
github.com/git-pkgs/purl v0.1.15
|
||||||
github.com/git-pkgs/registries v0.6.4
|
github.com/git-pkgs/registries v0.6.4
|
||||||
github.com/git-pkgs/spdx v0.1.4
|
github.com/git-pkgs/spdx v0.1.4
|
||||||
|
|
|
||||||
6
go.sum
6
go.sum
|
|
@ -244,12 +244,14 @@ github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
|
||||||
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
|
github.com/fzipp/gocyclo v0.6.0/go.mod h1:rXPyn8fnlpa0R2csP/31uerbiVBugk5whMdlyaLkLoA=
|
||||||
github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0=
|
github.com/ghostiam/protogetter v0.3.20 h1:oW7OPFit2FxZOpmMRPP9FffU4uUpfeE/rEdE1f+MzD0=
|
||||||
github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI=
|
github.com/ghostiam/protogetter v0.3.20/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI=
|
||||||
github.com/git-pkgs/archives v0.3.1 h1:GKUuw++0YXAAElxweVHiR4AaSShKKYoVQmyxlF5blG4=
|
github.com/git-pkgs/archives v0.4.0 h1:KNmmIsLiSH27lUdT27EfUkQXFaLgXV5KezE81iyOIgo=
|
||||||
github.com/git-pkgs/archives v0.3.1/go.mod h1:408oQv3FxLCtePa33zp3sg3njXnwH74vnHZFxkRqoPo=
|
github.com/git-pkgs/archives v0.4.0/go.mod h1:tfio0OIuPKEBKHs/UCL5XBUvYmKpnvtnba2iDlfSd6g=
|
||||||
github.com/git-pkgs/cooldown v0.1.1 h1:9OqqzCB8gANz/y44SmqGD0Jp8Qtu81D1sCbKl6Ehg7w=
|
github.com/git-pkgs/cooldown v0.1.1 h1:9OqqzCB8gANz/y44SmqGD0Jp8Qtu81D1sCbKl6Ehg7w=
|
||||||
github.com/git-pkgs/cooldown v0.1.1/go.mod h1:v7APuK/UouTiu8mWQZbdDmj7DfxxkGUeuhjaRB5gv9E=
|
github.com/git-pkgs/cooldown v0.1.1/go.mod h1:v7APuK/UouTiu8mWQZbdDmj7DfxxkGUeuhjaRB5gv9E=
|
||||||
github.com/git-pkgs/enrichment v0.6.4 h1:mGrfenttwmcUfPXRkWpB0wBJiiGj55ltniUh66Pq4bU=
|
github.com/git-pkgs/enrichment v0.6.4 h1:mGrfenttwmcUfPXRkWpB0wBJiiGj55ltniUh66Pq4bU=
|
||||||
github.com/git-pkgs/enrichment v0.6.4/go.mod h1:zz1vPUak/w8Jhajll0KDRN2MjKaEYeCzQTxumWnVhqY=
|
github.com/git-pkgs/enrichment v0.6.4/go.mod h1:zz1vPUak/w8Jhajll0KDRN2MjKaEYeCzQTxumWnVhqY=
|
||||||
|
github.com/git-pkgs/magic v0.1.0 h1:xLrqq7CMXB9g5bJnmJyKw17Rvlh0GFiEmO6e5RFsoeY=
|
||||||
|
github.com/git-pkgs/magic v0.1.0/go.mod h1:3ndidt+yvFaI1M0aEkkzkOlFnLPkeVQASIUojazcxCI=
|
||||||
github.com/git-pkgs/packageurl-go v0.3.1 h1:WM3RBABQZLaRBxgKyYughc3cVBE8KyQxbSC6Jt5ak7M=
|
github.com/git-pkgs/packageurl-go v0.3.1 h1:WM3RBABQZLaRBxgKyYughc3cVBE8KyQxbSC6Jt5ak7M=
|
||||||
github.com/git-pkgs/packageurl-go v0.3.1/go.mod h1:rcIxiG37BlQLB6FZfgdj9Fm7yjhRQd3l+5o7J0QPAk4=
|
github.com/git-pkgs/packageurl-go v0.3.1/go.mod h1:rcIxiG37BlQLB6FZfgdj9Fm7yjhRQd3l+5o7J0QPAk4=
|
||||||
github.com/git-pkgs/pom v0.1.5 h1:TGT8Az2OMxGWsXnSagtUMGzZm7Oax8HrSCteA+mi0qY=
|
github.com/git-pkgs/pom v0.1.5 h1:TGT8Az2OMxGWsXnSagtUMGzZm7Oax8HrSCteA+mi0qY=
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
package server
|
package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
@ -10,29 +11,22 @@ import (
|
||||||
|
|
||||||
"github.com/git-pkgs/archives"
|
"github.com/git-pkgs/archives"
|
||||||
"github.com/git-pkgs/archives/diff"
|
"github.com/git-pkgs/archives/diff"
|
||||||
|
"github.com/git-pkgs/magic"
|
||||||
"github.com/git-pkgs/proxy/internal/database"
|
"github.com/git-pkgs/proxy/internal/database"
|
||||||
"github.com/git-pkgs/purl"
|
"github.com/git-pkgs/purl"
|
||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
)
|
)
|
||||||
|
|
||||||
const contentTypePlainText = "text/plain; charset=utf-8"
|
const (
|
||||||
|
contentTypePlainText = "text/plain; charset=utf-8"
|
||||||
|
browseSniffSize = 512
|
||||||
|
)
|
||||||
|
|
||||||
// maxBrowseArchiveSize caps how much data openArchive will buffer for
|
// maxBrowseArchiveSize caps how much data openArchive will buffer for
|
||||||
// prefix detection. Artifacts larger than this are rejected to prevent
|
// prefix detection. Artifacts larger than this are rejected to prevent
|
||||||
// memory exhaustion from a single request.
|
// memory exhaustion from a single request.
|
||||||
const maxBrowseArchiveSize = 512 << 20 // 512 MB
|
const maxBrowseArchiveSize = 512 << 20 // 512 MB
|
||||||
|
|
||||||
// archiveFilename returns a filename suitable for archive format detection.
|
|
||||||
// Some ecosystems (e.g. composer) store artifacts with bare hash filenames
|
|
||||||
// that have no extension. This adds .zip when the original has no extension
|
|
||||||
// and the content is likely a zip archive.
|
|
||||||
func archiveFilename(filename string) string {
|
|
||||||
if path.Ext(filename) == "" {
|
|
||||||
return filename + ".zip"
|
|
||||||
}
|
|
||||||
return filename
|
|
||||||
}
|
|
||||||
|
|
||||||
// detectSingleRootDir returns the single top-level directory name if all files
|
// detectSingleRootDir returns the single top-level directory name if all files
|
||||||
// in the archive live under one common directory (e.g. GitHub zipballs use
|
// in the archive live under one common directory (e.g. GitHub zipballs use
|
||||||
// "repo-hash/"). Returns "" if there's no single root or the archive is flat.
|
// "repo-hash/"). Returns "" if there's no single root or the archive is flat.
|
||||||
|
|
@ -66,8 +60,6 @@ func detectSingleRootDir(reader archives.Reader) string {
|
||||||
// and stripping a single top-level directory prefix (like GitHub zipballs).
|
// and stripping a single top-level directory prefix (like GitHub zipballs).
|
||||||
// For npm, the hardcoded "package/" prefix takes precedence.
|
// For npm, the hardcoded "package/" prefix takes precedence.
|
||||||
func openArchive(filename string, content io.Reader, ecosystem string) (archives.Reader, error) { //nolint:ireturn // wraps multiple archive implementations
|
func openArchive(filename string, content io.Reader, ecosystem string) (archives.Reader, error) { //nolint:ireturn // wraps multiple archive implementations
|
||||||
fname := archiveFilename(filename)
|
|
||||||
|
|
||||||
limited := io.LimitReader(content, maxBrowseArchiveSize+1)
|
limited := io.LimitReader(content, maxBrowseArchiveSize+1)
|
||||||
data, err := io.ReadAll(limited)
|
data, err := io.ReadAll(limited)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -78,17 +70,17 @@ func openArchive(filename string, content io.Reader, ecosystem string) (archives
|
||||||
}
|
}
|
||||||
|
|
||||||
if ecosystem == "npm" {
|
if ecosystem == "npm" {
|
||||||
return archives.OpenBytesWithPrefix(fname, data, "package/")
|
return archives.OpenBytesWithPrefix(filename, data, "package/")
|
||||||
}
|
}
|
||||||
|
|
||||||
probe, err := archives.OpenBytes(fname, data)
|
probe, err := archives.OpenBytes(filename, data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
prefix := detectSingleRootDir(probe)
|
prefix := detectSingleRootDir(probe)
|
||||||
_ = probe.Close()
|
_ = probe.Close()
|
||||||
|
|
||||||
return archives.OpenBytesWithPrefix(fname, data, prefix)
|
return archives.OpenBytesWithPrefix(filename, data, prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
// BrowseListResponse contains the file listing for a directory in an archives.
|
// BrowseListResponse contains the file listing for a directory in an archives.
|
||||||
|
|
@ -361,7 +353,14 @@ func (s *Server) browseFile(w http.ResponseWriter, r *http.Request, ecosystem, n
|
||||||
}
|
}
|
||||||
defer func() { _ = fileReader.Close() }()
|
defer func() { _ = fileReader.Close() }()
|
||||||
|
|
||||||
contentType := detectContentType(filePath)
|
contentType, knownPath := detectContentTypeFromPath(filePath)
|
||||||
|
var content io.Reader = fileReader
|
||||||
|
if !knownPath {
|
||||||
|
bufferedFile := bufio.NewReaderSize(fileReader, browseSniffSize)
|
||||||
|
prefix, _ := bufferedFile.Peek(browseSniffSize)
|
||||||
|
contentType = detectContentType(filePath, prefix)
|
||||||
|
content = bufferedFile
|
||||||
|
}
|
||||||
w.Header().Set("Content-Type", contentType)
|
w.Header().Set("Content-Type", contentType)
|
||||||
w.Header().Set("Content-Security-Policy", "sandbox")
|
w.Header().Set("Content-Security-Policy", "sandbox")
|
||||||
w.Header().Set("X-Content-Type-Options", "nosniff")
|
w.Header().Set("X-Content-Type-Options", "nosniff")
|
||||||
|
|
@ -370,85 +369,112 @@ func (s *Server) browseFile(w http.ResponseWriter, r *http.Request, ecosystem, n
|
||||||
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", filename))
|
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", filename))
|
||||||
|
|
||||||
// Stream the file
|
// Stream the file
|
||||||
_, _ = io.Copy(w, fileReader)
|
_, _ = io.Copy(w, content)
|
||||||
}
|
}
|
||||||
|
|
||||||
// detectContentType returns an appropriate content type based on file extension.
|
// detectContentType returns an appropriate content type. Known filenames and
|
||||||
func detectContentType(filename string) string {
|
// extensions take precedence; content detection handles the remaining files.
|
||||||
|
func detectContentType(filename string, prefix []byte) string {
|
||||||
|
if contentType, ok := detectContentTypeFromPath(filename); ok {
|
||||||
|
return contentType
|
||||||
|
}
|
||||||
|
return detectContentTypeFromPrefix(prefix)
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectContentTypeFromPath(filename string) (string, bool) {
|
||||||
ext := strings.ToLower(path.Ext(filename))
|
ext := strings.ToLower(path.Ext(filename))
|
||||||
|
|
||||||
switch ext {
|
switch ext {
|
||||||
// Text formats
|
// Text formats
|
||||||
case ".txt", ".md", ".markdown":
|
case ".txt", ".md", ".markdown":
|
||||||
return contentTypePlainText
|
return contentTypePlainText, true
|
||||||
case ".html", ".htm", ".xhtml":
|
case ".html", ".htm", ".xhtml":
|
||||||
return contentTypePlainText
|
return contentTypePlainText, true
|
||||||
case ".css":
|
case ".css":
|
||||||
return "text/css; charset=utf-8"
|
return "text/css; charset=utf-8", true
|
||||||
case ".js", ".mjs":
|
case ".js", ".mjs":
|
||||||
return "application/javascript; charset=utf-8"
|
return "application/javascript; charset=utf-8", true
|
||||||
case ".json":
|
case ".json":
|
||||||
return "application/json; charset=utf-8"
|
return "application/json; charset=utf-8", true
|
||||||
case ".xml":
|
case ".xml":
|
||||||
return "application/xml; charset=utf-8"
|
return "application/xml; charset=utf-8", true
|
||||||
case ".yaml", ".yml":
|
case ".yaml", ".yml":
|
||||||
return "text/yaml; charset=utf-8"
|
return "text/yaml; charset=utf-8", true
|
||||||
case ".toml":
|
case ".toml":
|
||||||
return "text/toml; charset=utf-8"
|
return "text/toml; charset=utf-8", true
|
||||||
|
|
||||||
// Programming languages
|
// Programming languages
|
||||||
case ".go":
|
case ".go":
|
||||||
return "text/x-go; charset=utf-8"
|
return "text/x-go; charset=utf-8", true
|
||||||
case ".rs":
|
case ".rs":
|
||||||
return "text/x-rust; charset=utf-8"
|
return "text/x-rust; charset=utf-8", true
|
||||||
case ".py":
|
case ".py":
|
||||||
return "text/x-python; charset=utf-8"
|
return "text/x-python; charset=utf-8", true
|
||||||
case ".rb":
|
case ".rb":
|
||||||
return "text/x-ruby; charset=utf-8"
|
return "text/x-ruby; charset=utf-8", true
|
||||||
case ".java":
|
case ".java":
|
||||||
return "text/x-java; charset=utf-8"
|
return "text/x-java; charset=utf-8", true
|
||||||
case ".c", ".h":
|
case ".c", ".h":
|
||||||
return "text/x-c; charset=utf-8"
|
return "text/x-c; charset=utf-8", true
|
||||||
case ".cpp", ".cc", ".cxx", ".hpp":
|
case ".cpp", ".cc", ".cxx", ".hpp":
|
||||||
return "text/x-c++; charset=utf-8"
|
return "text/x-c++; charset=utf-8", true
|
||||||
case ".ts":
|
case ".ts":
|
||||||
return "text/typescript; charset=utf-8"
|
return "text/typescript; charset=utf-8", true
|
||||||
case ".tsx":
|
case ".tsx":
|
||||||
return "text/tsx; charset=utf-8"
|
return "text/tsx; charset=utf-8", true
|
||||||
case ".jsx":
|
case ".jsx":
|
||||||
return "text/jsx; charset=utf-8"
|
return "text/jsx; charset=utf-8", true
|
||||||
case ".php":
|
case ".php":
|
||||||
return "text/x-php; charset=utf-8"
|
return "text/x-php; charset=utf-8", true
|
||||||
|
|
||||||
// Config files
|
// Config files
|
||||||
case ".conf", ".config", ".ini":
|
case ".conf", ".config", ".ini":
|
||||||
return contentTypePlainText
|
return contentTypePlainText, true
|
||||||
case ".sh", ".bash":
|
case ".sh", ".bash":
|
||||||
return "text/x-shellscript; charset=utf-8"
|
return "text/x-shellscript; charset=utf-8", true
|
||||||
case ".dockerfile":
|
case ".dockerfile":
|
||||||
return "text/x-dockerfile; charset=utf-8"
|
return "text/x-dockerfile; charset=utf-8", true
|
||||||
|
|
||||||
// Images
|
// Images
|
||||||
case ".png":
|
case ".png":
|
||||||
return "image/png"
|
return "image/png", true
|
||||||
case ".jpg", ".jpeg":
|
case ".jpg", ".jpeg":
|
||||||
return "image/jpeg"
|
return "image/jpeg", true
|
||||||
case ".gif":
|
case ".gif":
|
||||||
return "image/gif"
|
return "image/gif", true
|
||||||
case ".svg":
|
case ".svg":
|
||||||
return contentTypePlainText
|
return contentTypePlainText, true
|
||||||
case ".ico":
|
case ".ico":
|
||||||
return "image/x-icon"
|
return "image/x-icon", true
|
||||||
|
|
||||||
// Archives
|
// Archives
|
||||||
case ".zip", ".tar", ".gz", ".bz2", ".xz":
|
case ".zip", ".tar", ".gz", ".bz2", ".xz":
|
||||||
return "application/octet-stream"
|
return "application/octet-stream", true
|
||||||
|
|
||||||
default:
|
default:
|
||||||
// Try to detect if it looks like text
|
|
||||||
if isLikelyText(filename) {
|
if isLikelyText(filename) {
|
||||||
return contentTypePlainText
|
return contentTypePlainText, true
|
||||||
}
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectContentTypeFromPrefix(prefix []byte) string {
|
||||||
|
result := magic.DetectPrefix(prefix)
|
||||||
|
if result.Kind == magic.KindText {
|
||||||
|
return contentTypePlainText
|
||||||
|
}
|
||||||
|
|
||||||
|
switch result.Format {
|
||||||
|
case "png":
|
||||||
|
return "image/png"
|
||||||
|
case "jpeg":
|
||||||
|
return "image/jpeg"
|
||||||
|
case "gif":
|
||||||
|
return "image/gif"
|
||||||
|
case "pdf":
|
||||||
|
return "application/pdf"
|
||||||
|
default:
|
||||||
return "application/octet-stream"
|
return "application/octet-stream"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -55,3 +55,28 @@ func BenchmarkOpenArchive(b *testing.B) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func BenchmarkDetectContentType(b *testing.B) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
filename string
|
||||||
|
prefix []byte
|
||||||
|
}{
|
||||||
|
{"known-path", "README.md", nil},
|
||||||
|
{"text-prefix", "artifact", bytes.Repeat([]byte("a"), browseSniffSize)},
|
||||||
|
{"png-prefix", "artifact", append([]byte("\x89PNG\r\n\x1a\n"), make([]byte, browseSniffSize-8)...)},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
b.Run(tc.name, func(b *testing.B) {
|
||||||
|
b.ReportAllocs()
|
||||||
|
var contentType string
|
||||||
|
for b.Loop() {
|
||||||
|
contentType = detectContentType(tc.filename, tc.prefix)
|
||||||
|
}
|
||||||
|
if contentType == "" {
|
||||||
|
b.Fatal("empty content type")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -137,29 +137,44 @@ func TestHandleBrowseFile(t *testing.T) {
|
||||||
t.Fatalf("failed to upsert artifact: %v", err)
|
t.Fatalf("failed to upsert artifact: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test fetching a file
|
files := []struct {
|
||||||
req := httptest.NewRequest("GET", "/ui/api/browse/npm/test-browse/1.0.0/file/README.md", nil)
|
path string
|
||||||
w := httptest.NewRecorder()
|
content string
|
||||||
ts.handler.ServeHTTP(w, req)
|
contentType string
|
||||||
|
}{
|
||||||
if w.Code != http.StatusOK {
|
{"README.md", "# Test Package\n", contentTypePlainText},
|
||||||
t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
{"notes.data", "short text\n", contentTypePlainText},
|
||||||
|
{"logo", "\x89PNG\r\n\x1a\nimage data", "image/png"},
|
||||||
|
{"page", "<!DOCTYPE html><html></html>", contentTypePlainText},
|
||||||
|
{"misleading.txt", "\x89PNG\r\n\x1a\nimage data", contentTypePlainText},
|
||||||
}
|
}
|
||||||
|
for _, file := range files {
|
||||||
|
t.Run(file.path, func(t *testing.T) {
|
||||||
|
req := httptest.NewRequest("GET", "/ui/api/browse/npm/test-browse/1.0.0/file/"+file.path, nil)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
ts.handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
body := w.Body.String()
|
if w.Code != http.StatusOK {
|
||||||
if body != "# Test Package\n" {
|
t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String())
|
||||||
t.Errorf("unexpected file content: %q", body)
|
}
|
||||||
}
|
if w.Body.String() != file.content {
|
||||||
|
t.Errorf("unexpected file content: %q", w.Body.String())
|
||||||
// Check content type
|
}
|
||||||
contentType := w.Header().Get("Content-Type")
|
if got := w.Header().Get("Content-Type"); got != file.contentType {
|
||||||
if contentType != contentTypePlainText {
|
t.Errorf("Content-Type = %q, want %q", got, file.contentType)
|
||||||
t.Errorf("expected text/plain content type, got %q", contentType)
|
}
|
||||||
|
if got := w.Header().Get("Content-Security-Policy"); got != "sandbox" {
|
||||||
|
t.Errorf("Content-Security-Policy = %q, want sandbox", got)
|
||||||
|
}
|
||||||
|
if got := w.Header().Get("X-Content-Type-Options"); got != "nosniff" {
|
||||||
|
t.Errorf("X-Content-Type-Options = %q, want nosniff", got)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test fetching non-existent file
|
// Test fetching non-existent file
|
||||||
req = httptest.NewRequest("GET", "/ui/api/browse/npm/test-browse/1.0.0/file/nonexistent.txt", nil)
|
req := httptest.NewRequest("GET", "/ui/api/browse/npm/test-browse/1.0.0/file/nonexistent.txt", nil)
|
||||||
w = httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
ts.handler.ServeHTTP(w, req)
|
ts.handler.ServeHTTP(w, req)
|
||||||
|
|
||||||
if w.Code != http.StatusNotFound {
|
if w.Code != http.StatusNotFound {
|
||||||
|
|
@ -169,34 +184,47 @@ func TestHandleBrowseFile(t *testing.T) {
|
||||||
|
|
||||||
func TestDetectContentType(t *testing.T) {
|
func TestDetectContentType(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
|
name string
|
||||||
filename string
|
filename string
|
||||||
|
prefix []byte
|
||||||
expectedCT string
|
expectedCT string
|
||||||
}{
|
}{
|
||||||
{"file.txt", contentTypePlainText},
|
{"text extension", "file.txt", nil, contentTypePlainText},
|
||||||
{"file.md", contentTypePlainText},
|
{"markdown extension", "file.md", nil, contentTypePlainText},
|
||||||
{"file.json", "application/json; charset=utf-8"},
|
{"JSON extension", "file.json", nil, "application/json; charset=utf-8"},
|
||||||
{"file.js", "application/javascript; charset=utf-8"},
|
{"JavaScript extension", "file.js", nil, "application/javascript; charset=utf-8"},
|
||||||
{"file.go", "text/x-go; charset=utf-8"},
|
{"Go extension", "file.go", nil, "text/x-go; charset=utf-8"},
|
||||||
{"file.py", "text/x-python; charset=utf-8"},
|
{"Python extension", "file.py", nil, "text/x-python; charset=utf-8"},
|
||||||
{"file.rs", "text/x-rust; charset=utf-8"},
|
{"Rust extension", "file.rs", nil, "text/x-rust; charset=utf-8"},
|
||||||
{"file.html", contentTypePlainText},
|
{"HTML extension", "file.html", nil, contentTypePlainText},
|
||||||
{"file.htm", contentTypePlainText},
|
{"HTM extension", "file.htm", nil, contentTypePlainText},
|
||||||
{"file.xhtml", contentTypePlainText},
|
{"XHTML extension", "file.xhtml", nil, contentTypePlainText},
|
||||||
{"file.svg", contentTypePlainText},
|
{"SVG extension", "file.svg", nil, contentTypePlainText},
|
||||||
{"file.png", "image/png"},
|
{"PNG extension", "file.png", nil, "image/png"},
|
||||||
{"file.jpg", "image/jpeg"},
|
{"JPEG extension", "file.jpg", nil, "image/jpeg"},
|
||||||
{"README", contentTypePlainText},
|
{"README", "README", nil, contentTypePlainText},
|
||||||
{"LICENSE", contentTypePlainText},
|
{"LICENSE", "LICENSE", nil, contentTypePlainText},
|
||||||
{"Makefile", contentTypePlainText},
|
{"Makefile", "Makefile", nil, contentTypePlainText},
|
||||||
{".gitignore", contentTypePlainText},
|
{"gitignore", ".gitignore", nil, contentTypePlainText},
|
||||||
{"file.bin", "application/octet-stream"},
|
{"unknown empty", "file.bin", nil, "application/octet-stream"},
|
||||||
|
{"extensionless PNG", "asset", []byte("\x89PNG\r\n\x1a\n"), "image/png"},
|
||||||
|
{"extensionless JPEG", "asset", []byte("\xff\xd8\xff"), "image/jpeg"},
|
||||||
|
{"extensionless GIF", "asset", []byte("GIF89a"), "image/gif"},
|
||||||
|
{"extensionless PDF", "asset", []byte("%PDF-1.7"), "application/pdf"},
|
||||||
|
{"extensionless text", "asset", []byte("plain text\n"), contentTypePlainText},
|
||||||
|
{"extensionless HTML", "asset", []byte("<!DOCTYPE html><html></html>"), contentTypePlainText},
|
||||||
|
{"extensionless XML", "asset", []byte("<?xml version=\"1.0\"?><root/>"), contentTypePlainText},
|
||||||
|
{"extensionless SVG", "asset", []byte("<svg xmlns=\"http://www.w3.org/2000/svg\"></svg>"), contentTypePlainText},
|
||||||
|
{"extensionless ZIP", "asset", []byte("PK\x03\x04"), "application/octet-stream"},
|
||||||
|
{"extensionless binary", "asset", []byte{0, 1, 2}, "application/octet-stream"},
|
||||||
|
{"known path wins", "file.txt", []byte("\x89PNG\r\n\x1a\n"), contentTypePlainText},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.filename, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
got := detectContentType(tt.filename)
|
got := detectContentType(tt.filename, tt.prefix)
|
||||||
if got != tt.expectedCT {
|
if got != tt.expectedCT {
|
||||||
t.Errorf("detectContentType(%q) = %q, want %q", tt.filename, got, tt.expectedCT)
|
t.Errorf("detectContentType(%q, %q) = %q, want %q", tt.filename, tt.prefix, got, tt.expectedCT)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -255,6 +283,10 @@ func createTestArchive(t *testing.T) []byte {
|
||||||
"package/lib/index.js": "module.exports = {};",
|
"package/lib/index.js": "module.exports = {};",
|
||||||
"package/lib/helper.js": "module.exports.help = () => {};",
|
"package/lib/helper.js": "module.exports.help = () => {};",
|
||||||
"package/test/index.test.js": "// tests",
|
"package/test/index.test.js": "// tests",
|
||||||
|
"package/notes.data": "short text\n",
|
||||||
|
"package/logo": "\x89PNG\r\n\x1a\nimage data",
|
||||||
|
"package/page": "<!DOCTYPE html><html></html>",
|
||||||
|
"package/misleading.txt": "\x89PNG\r\n\x1a\nimage data",
|
||||||
}
|
}
|
||||||
|
|
||||||
for path, content := range files {
|
for path, content := range files {
|
||||||
|
|
@ -609,25 +641,19 @@ func TestHandleComparePage(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestArchiveFilename(t *testing.T) {
|
func TestOpenArchiveDetectsExtensionlessTarGz(t *testing.T) {
|
||||||
tests := []struct {
|
reader, err := openArchive("artifact", bytes.NewReader(createTestArchive(t)), "npm")
|
||||||
input string
|
if err != nil {
|
||||||
want string
|
t.Fatalf("openArchive failed: %v", err)
|
||||||
}{
|
|
||||||
{"package.tar.gz", "package.tar.gz"},
|
|
||||||
{"d2e2f014ccd6ec9fae8dbe6336a4164346a2a856", "d2e2f014ccd6ec9fae8dbe6336a4164346a2a856.zip"},
|
|
||||||
{"file.zip", "file.zip"},
|
|
||||||
{"archive.tgz", "archive.tgz"},
|
|
||||||
{"noext", "noext.zip"},
|
|
||||||
}
|
}
|
||||||
|
defer func() { _ = reader.Close() }()
|
||||||
|
|
||||||
for _, tt := range tests {
|
files, err := reader.List()
|
||||||
t.Run(tt.input, func(t *testing.T) {
|
if err != nil {
|
||||||
got := archiveFilename(tt.input)
|
t.Fatalf("List failed: %v", err)
|
||||||
if got != tt.want {
|
}
|
||||||
t.Errorf("archiveFilename(%q) = %q, want %q", tt.input, got, tt.want)
|
if len(files) == 0 {
|
||||||
}
|
t.Fatal("expected files in extensionless archive")
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue