diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index bb3d87d..c405f5d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -17,7 +17,7 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
@@ -35,7 +35,7 @@ jobs:
lint:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index c08cfa6..9c99e5d 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -20,12 +20,12 @@ jobs:
contents: read
steps:
- name: Check out the repo
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
with:
persist-credentials: false
- name: Log in to the Container registry
- uses: docker/login-action@06fb636fac595d6fb4b28a5dfcb21a6f5091859c
+ uses: docker/login-action@af1e73f918a031802d376d3c8bbc3fe56130a9b0
with:
registry: ghcr.io
username: ${{ github.actor }}
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 5d32181..f06cecd 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
diff --git a/.github/workflows/swagger.yml b/.github/workflows/swagger.yml
index 625f944..e99aecf 100644
--- a/.github/workflows/swagger.yml
+++ b/.github/workflows/swagger.yml
@@ -12,7 +12,7 @@ jobs:
swagger:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
diff --git a/.github/workflows/zizmor.yml b/.github/workflows/zizmor.yml
index 4df0e18..df06c5f 100644
--- a/.github/workflows/zizmor.yml
+++ b/.github/workflows/zizmor.yml
@@ -21,9 +21,9 @@ jobs:
security-events: write
steps:
- name: Checkout
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run zizmor
- uses: zizmorcore/zizmor-action@6fc4b006235f201fdab3722e17240ab420d580e5 # v0.6.1
+ uses: zizmorcore/zizmor-action@6599ee8b7a49aef6a770f63d261d214911a7ce02 # v0.6.0
diff --git a/go.mod b/go.mod
index e6e0a08..c460ec7 100644
--- a/go.mod
+++ b/go.mod
@@ -5,10 +5,9 @@ go 1.25.6
require (
github.com/BurntSushi/toml v1.6.0
github.com/CycloneDX/cyclonedx-go v0.11.0
- github.com/git-pkgs/archives v0.4.0
+ github.com/git-pkgs/archives v0.3.1
github.com/git-pkgs/cooldown v0.1.1
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/registries v0.6.4
github.com/git-pkgs/spdx v0.1.4
@@ -17,7 +16,7 @@ require (
github.com/go-chi/chi/v5 v5.3.1
github.com/jmoiron/sqlx v1.4.0
github.com/lib/pq v1.12.3
- github.com/prometheus/client_golang v1.24.0
+ github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2
github.com/spdx/tools-golang v0.5.7
github.com/swaggo/swag v1.16.6
@@ -25,7 +24,7 @@ require (
golang.org/x/sync v0.22.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
- modernc.org/sqlite v1.55.0
+ modernc.org/sqlite v1.54.0
)
require (
@@ -226,8 +225,8 @@ require (
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
- github.com/prometheus/common v0.70.0 // indirect
- github.com/prometheus/procfs v0.21.1 // indirect
+ github.com/prometheus/common v0.67.5 // indirect
+ github.com/prometheus/procfs v0.20.1 // indirect
github.com/quasilyte/go-ruleguard v0.4.5 // indirect
github.com/quasilyte/go-ruleguard/dsl v0.3.23 // indirect
github.com/quasilyte/gogrep v0.5.0 // indirect
@@ -291,7 +290,7 @@ require (
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.1 // indirect
- go.yaml.in/yaml/v2 v2.4.4 // indirect
+ go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/exp v0.0.0-20260218203240-3dfff04db8fa // indirect
@@ -299,7 +298,7 @@ require (
golang.org/x/mod v0.37.0 // indirect
golang.org/x/net v0.56.0 // indirect
golang.org/x/oauth2 v0.36.0 // indirect
- golang.org/x/sys v0.47.0 // indirect
+ golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.38.0 // indirect
golang.org/x/tools v0.47.0 // indirect
golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect
diff --git a/go.sum b/go.sum
index c239240..5621b47 100644
--- a/go.sum
+++ b/go.sum
@@ -244,14 +244,12 @@ github.com/fzipp/gocyclo v0.6.0 h1:lsblElZG7d3ALtGMx9fmxeTKZaLLpU8mET09yN4BBLo=
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/go.mod h1:FjIu5Yfs6FT391m+Fjp3fbAYJ6rkL/J6ySpZBfnODuI=
-github.com/git-pkgs/archives v0.4.0 h1:KNmmIsLiSH27lUdT27EfUkQXFaLgXV5KezE81iyOIgo=
-github.com/git-pkgs/archives v0.4.0/go.mod h1:tfio0OIuPKEBKHs/UCL5XBUvYmKpnvtnba2iDlfSd6g=
+github.com/git-pkgs/archives v0.3.1 h1:GKUuw++0YXAAElxweVHiR4AaSShKKYoVQmyxlF5blG4=
+github.com/git-pkgs/archives v0.3.1/go.mod h1:408oQv3FxLCtePa33zp3sg3njXnwH74vnHZFxkRqoPo=
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/enrichment v0.6.4 h1:mGrfenttwmcUfPXRkWpB0wBJiiGj55ltniUh66Pq4bU=
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/go.mod h1:rcIxiG37BlQLB6FZfgdj9Fm7yjhRQd3l+5o7J0QPAk4=
github.com/git-pkgs/pom v0.1.5 h1:TGT8Az2OMxGWsXnSagtUMGzZm7Oax8HrSCteA+mi0qY=
@@ -424,8 +422,8 @@ github.com/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3
github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8=
github.com/kkHAIKE/contextcheck v1.1.6 h1:7HIyRcnyzxL9Lz06NGhiKvenXq7Zw6Q0UQu/ttjfJCE=
github.com/kkHAIKE/contextcheck v1.1.6/go.mod h1:3dDbMRNBFaq8HFXWC1JyvDSPm43CmE6IuHam8Wr0rkg=
-github.com/klauspost/compress v1.19.0 h1:sXLILfc9jV2QYWkzFOPWStmcUVH2RHEB1JCdY2oVvCQ=
-github.com/klauspost/compress v1.19.0/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
+github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
@@ -543,14 +541,14 @@ github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgm
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/prometheus/client_golang v1.24.0 h1:5XStIklKuAtJSNpdD3s8XJj/Yv78IQmE1kbNk87JrAI=
-github.com/prometheus/client_golang v1.24.0/go.mod h1:QcsNdotprC2nS4BTM2ucbcqxd2CeXTEa9jW7zHO9iDE=
+github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
+github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
-github.com/prometheus/common v0.70.0 h1:bcpru3tWPVnxGnETLgOV5jbp/JRXgYEyv65CuBLAMMI=
-github.com/prometheus/common v0.70.0/go.mod h1:S/SFasQmgGiYH6C81LKCtYa8QACgthGg5zxL2udV7SY=
-github.com/prometheus/procfs v0.21.1 h1:GljZCt+zSTS+NZq88cyQ1LjZ+RCHp3uVuabBWA5+OJI=
-github.com/prometheus/procfs v0.21.1/go.mod h1:aB55Cww9pdSJVHk0hUf0inxWyyjPogFIjmHKYgMKmtY=
+github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
+github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
+github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
+github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/quasilyte/go-ruleguard v0.4.5 h1:AGY0tiOT5hJX9BTdx/xBdoCubQUAE2grkqY2lSwvZcA=
github.com/quasilyte/go-ruleguard v0.4.5/go.mod h1:Vl05zJ538vcEEwu16V/Hdu7IYZWyKSwIy4c88Ro1kRE=
github.com/quasilyte/go-ruleguard/dsl v0.3.23 h1:lxjt5B6ZCiBeeNO8/oQsegE6fLeCzuMRoVWSkXC4uvY=
@@ -728,8 +726,8 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
-go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
-go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
+go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
+go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
gocloud.dev v0.46.0 h1:niIuZwSjMtBx8K+ITB2s5kZullB13PGOS2ZoQPZxQ4Q=
@@ -809,8 +807,8 @@ golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
-golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
+golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -906,8 +904,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
-modernc.org/sqlite v1.55.0 h1:hIFh0MCH0rGinQ/4KYb5/UbCkRkb+UP+OkLCVWa5MTM=
-modernc.org/sqlite v1.55.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
+modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog=
+modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
diff --git a/internal/server/browse.go b/internal/server/browse.go
index 56aa1c5..504f5f1 100644
--- a/internal/server/browse.go
+++ b/internal/server/browse.go
@@ -1,7 +1,6 @@
package server
import (
- "bufio"
"encoding/json"
"fmt"
"io"
@@ -11,22 +10,29 @@ import (
"github.com/git-pkgs/archives"
"github.com/git-pkgs/archives/diff"
- "github.com/git-pkgs/magic"
"github.com/git-pkgs/proxy/internal/database"
"github.com/git-pkgs/purl"
"github.com/go-chi/chi/v5"
)
-const (
- contentTypePlainText = "text/plain; charset=utf-8"
- browseSniffSize = 512
-)
+const contentTypePlainText = "text/plain; charset=utf-8"
// maxBrowseArchiveSize caps how much data openArchive will buffer for
// prefix detection. Artifacts larger than this are rejected to prevent
// memory exhaustion from a single request.
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
// 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.
@@ -60,6 +66,8 @@ func detectSingleRootDir(reader archives.Reader) string {
// and stripping a single top-level directory prefix (like GitHub zipballs).
// 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
+ fname := archiveFilename(filename)
+
limited := io.LimitReader(content, maxBrowseArchiveSize+1)
data, err := io.ReadAll(limited)
if err != nil {
@@ -70,17 +78,17 @@ func openArchive(filename string, content io.Reader, ecosystem string) (archives
}
if ecosystem == "npm" {
- return archives.OpenBytesWithPrefix(filename, data, "package/")
+ return archives.OpenBytesWithPrefix(fname, data, "package/")
}
- probe, err := archives.OpenBytes(filename, data)
+ probe, err := archives.OpenBytes(fname, data)
if err != nil {
return nil, err
}
prefix := detectSingleRootDir(probe)
_ = probe.Close()
- return archives.OpenBytesWithPrefix(filename, data, prefix)
+ return archives.OpenBytesWithPrefix(fname, data, prefix)
}
// BrowseListResponse contains the file listing for a directory in an archives.
@@ -353,14 +361,7 @@ func (s *Server) browseFile(w http.ResponseWriter, r *http.Request, ecosystem, n
}
defer func() { _ = fileReader.Close() }()
- contentType, knownPath := detectContentTypeFromPath(filePath)
- var content io.Reader = fileReader
- if !knownPath {
- bufferedFile := bufio.NewReaderSize(fileReader, browseSniffSize)
- prefix, _ := bufferedFile.Peek(browseSniffSize)
- contentType = detectContentTypeFromPrefix(prefix)
- content = bufferedFile
- }
+ contentType := detectContentType(filePath)
w.Header().Set("Content-Type", contentType)
w.Header().Set("Content-Security-Policy", "sandbox")
w.Header().Set("X-Content-Type-Options", "nosniff")
@@ -369,103 +370,85 @@ func (s *Server) browseFile(w http.ResponseWriter, r *http.Request, ecosystem, n
w.Header().Set("Content-Disposition", fmt.Sprintf("inline; filename=%q", filename))
// Stream the file
- _, _ = io.Copy(w, content)
+ _, _ = io.Copy(w, fileReader)
}
-func detectContentTypeFromPath(filename string) (string, bool) {
+// detectContentType returns an appropriate content type based on file extension.
+func detectContentType(filename string) string {
ext := strings.ToLower(path.Ext(filename))
switch ext {
// Text formats
case ".txt", ".md", ".markdown":
- return contentTypePlainText, true
+ return contentTypePlainText
case ".html", ".htm", ".xhtml":
- return contentTypePlainText, true
+ return contentTypePlainText
case ".css":
- return "text/css; charset=utf-8", true
+ return "text/css; charset=utf-8"
case ".js", ".mjs":
- return "application/javascript; charset=utf-8", true
+ return "application/javascript; charset=utf-8"
case ".json":
- return "application/json; charset=utf-8", true
+ return "application/json; charset=utf-8"
case ".xml":
- return "application/xml; charset=utf-8", true
+ return "application/xml; charset=utf-8"
case ".yaml", ".yml":
- return "text/yaml; charset=utf-8", true
+ return "text/yaml; charset=utf-8"
case ".toml":
- return "text/toml; charset=utf-8", true
+ return "text/toml; charset=utf-8"
// Programming languages
case ".go":
- return "text/x-go; charset=utf-8", true
+ return "text/x-go; charset=utf-8"
case ".rs":
- return "text/x-rust; charset=utf-8", true
+ return "text/x-rust; charset=utf-8"
case ".py":
- return "text/x-python; charset=utf-8", true
+ return "text/x-python; charset=utf-8"
case ".rb":
- return "text/x-ruby; charset=utf-8", true
+ return "text/x-ruby; charset=utf-8"
case ".java":
- return "text/x-java; charset=utf-8", true
+ return "text/x-java; charset=utf-8"
case ".c", ".h":
- return "text/x-c; charset=utf-8", true
+ return "text/x-c; charset=utf-8"
case ".cpp", ".cc", ".cxx", ".hpp":
- return "text/x-c++; charset=utf-8", true
+ return "text/x-c++; charset=utf-8"
case ".ts":
- return "text/typescript; charset=utf-8", true
+ return "text/typescript; charset=utf-8"
case ".tsx":
- return "text/tsx; charset=utf-8", true
+ return "text/tsx; charset=utf-8"
case ".jsx":
- return "text/jsx; charset=utf-8", true
+ return "text/jsx; charset=utf-8"
case ".php":
- return "text/x-php; charset=utf-8", true
+ return "text/x-php; charset=utf-8"
// Config files
case ".conf", ".config", ".ini":
- return contentTypePlainText, true
+ return contentTypePlainText
case ".sh", ".bash":
- return "text/x-shellscript; charset=utf-8", true
+ return "text/x-shellscript; charset=utf-8"
case ".dockerfile":
- return "text/x-dockerfile; charset=utf-8", true
+ return "text/x-dockerfile; charset=utf-8"
// Images
case ".png":
- return "image/png", true
+ return "image/png"
case ".jpg", ".jpeg":
- return "image/jpeg", true
+ return "image/jpeg"
case ".gif":
- return "image/gif", true
+ return "image/gif"
case ".svg":
- return contentTypePlainText, true
+ return contentTypePlainText
case ".ico":
- return "image/x-icon", true
+ return "image/x-icon"
// Archives
case ".zip", ".tar", ".gz", ".bz2", ".xz":
- return "application/octet-stream", true
+ return "application/octet-stream"
default:
+ // Try to detect if it looks like text
if isLikelyText(filename) {
- return contentTypePlainText, true
+ return contentTypePlainText
}
- 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"
}
}
diff --git a/internal/server/browse_bench_test.go b/internal/server/browse_bench_test.go
index 840bc75..03f3f02 100644
--- a/internal/server/browse_bench_test.go
+++ b/internal/server/browse_bench_test.go
@@ -55,35 +55,3 @@ func BenchmarkOpenArchive(b *testing.B) {
})
}
}
-
-func BenchmarkDetectContentType(b *testing.B) {
- cases := []struct {
- name string
- filename string
- prefix []byte
- knownPath bool
- }{
- {"known-path", "README.md", nil, true},
- {"text-prefix", "artifact", bytes.Repeat([]byte("a"), browseSniffSize), false},
- {"png-prefix", "artifact", append([]byte("\x89PNG\r\n\x1a\n"), make([]byte, browseSniffSize-8)...), false},
- }
-
- for _, tc := range cases {
- b.Run(tc.name, func(b *testing.B) {
- b.ReportAllocs()
- var contentType string
- if tc.knownPath {
- for b.Loop() {
- contentType, _ = detectContentTypeFromPath(tc.filename)
- }
- } else {
- for b.Loop() {
- contentType = detectContentTypeFromPrefix(tc.prefix)
- }
- }
- if contentType == "" {
- b.Fatal("empty content type")
- }
- })
- }
-}
diff --git a/internal/server/browse_test.go b/internal/server/browse_test.go
index 5240a92..f1fb993 100644
--- a/internal/server/browse_test.go
+++ b/internal/server/browse_test.go
@@ -137,44 +137,29 @@ func TestHandleBrowseFile(t *testing.T) {
t.Fatalf("failed to upsert artifact: %v", err)
}
- files := []struct {
- path string
- content string
- contentType string
- }{
- {"README.md", "# Test Package\n", contentTypePlainText},
- {"notes.data", "short text\n", contentTypePlainText},
- {"logo", "\x89PNG\r\n\x1a\nimage data", "image/png"},
- {"page", "", 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)
+ // Test fetching a file
+ req := httptest.NewRequest("GET", "/ui/api/browse/npm/test-browse/1.0.0/file/README.md", nil)
+ w := httptest.NewRecorder()
+ ts.handler.ServeHTTP(w, req)
- if w.Code != http.StatusOK {
- t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String())
- }
- if w.Body.String() != file.content {
- t.Errorf("unexpected file content: %q", w.Body.String())
- }
- if got := w.Header().Get("Content-Type"); got != file.contentType {
- t.Errorf("Content-Type = %q, want %q", got, file.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)
- }
- })
+ if w.Code != http.StatusOK {
+ t.Fatalf("expected status 200, got %d: %s", w.Code, w.Body.String())
+ }
+
+ body := w.Body.String()
+ if body != "# Test Package\n" {
+ t.Errorf("unexpected file content: %q", body)
+ }
+
+ // Check content type
+ contentType := w.Header().Get("Content-Type")
+ if contentType != contentTypePlainText {
+ t.Errorf("expected text/plain content type, got %q", contentType)
}
// Test fetching non-existent file
- req := httptest.NewRequest("GET", "/ui/api/browse/npm/test-browse/1.0.0/file/nonexistent.txt", nil)
- w := httptest.NewRecorder()
+ req = httptest.NewRequest("GET", "/ui/api/browse/npm/test-browse/1.0.0/file/nonexistent.txt", nil)
+ w = httptest.NewRecorder()
ts.handler.ServeHTTP(w, req)
if w.Code != http.StatusNotFound {
@@ -182,52 +167,36 @@ func TestHandleBrowseFile(t *testing.T) {
}
}
-func TestBrowseContentTypePolicy(t *testing.T) {
+func TestDetectContentType(t *testing.T) {
tests := []struct {
- name string
filename string
- prefix []byte
expectedCT string
}{
- {"text extension", "file.txt", nil, contentTypePlainText},
- {"markdown extension", "file.md", nil, contentTypePlainText},
- {"JSON extension", "file.json", nil, "application/json; charset=utf-8"},
- {"JavaScript extension", "file.js", nil, "application/javascript; charset=utf-8"},
- {"Go extension", "file.go", nil, "text/x-go; charset=utf-8"},
- {"Python extension", "file.py", nil, "text/x-python; charset=utf-8"},
- {"Rust extension", "file.rs", nil, "text/x-rust; charset=utf-8"},
- {"HTML extension", "file.html", nil, contentTypePlainText},
- {"HTM extension", "file.htm", nil, contentTypePlainText},
- {"XHTML extension", "file.xhtml", nil, contentTypePlainText},
- {"SVG extension", "file.svg", nil, contentTypePlainText},
- {"PNG extension", "file.png", nil, "image/png"},
- {"JPEG extension", "file.jpg", nil, "image/jpeg"},
- {"README", "README", nil, contentTypePlainText},
- {"LICENSE", "LICENSE", nil, contentTypePlainText},
- {"Makefile", "Makefile", nil, contentTypePlainText},
- {"gitignore", ".gitignore", nil, contentTypePlainText},
- {"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(""), contentTypePlainText},
- {"extensionless XML", "asset", []byte(""), contentTypePlainText},
- {"extensionless SVG", "asset", []byte(""), 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},
+ {"file.txt", contentTypePlainText},
+ {"file.md", contentTypePlainText},
+ {"file.json", "application/json; charset=utf-8"},
+ {"file.js", "application/javascript; charset=utf-8"},
+ {"file.go", "text/x-go; charset=utf-8"},
+ {"file.py", "text/x-python; charset=utf-8"},
+ {"file.rs", "text/x-rust; charset=utf-8"},
+ {"file.html", contentTypePlainText},
+ {"file.htm", contentTypePlainText},
+ {"file.xhtml", contentTypePlainText},
+ {"file.svg", contentTypePlainText},
+ {"file.png", "image/png"},
+ {"file.jpg", "image/jpeg"},
+ {"README", contentTypePlainText},
+ {"LICENSE", contentTypePlainText},
+ {"Makefile", contentTypePlainText},
+ {".gitignore", contentTypePlainText},
+ {"file.bin", "application/octet-stream"},
}
for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- got, knownPath := detectContentTypeFromPath(tt.filename)
- if !knownPath {
- got = detectContentTypeFromPrefix(tt.prefix)
- }
+ t.Run(tt.filename, func(t *testing.T) {
+ got := detectContentType(tt.filename)
if got != tt.expectedCT {
- t.Errorf("content type for %q with prefix %q = %q, want %q", tt.filename, tt.prefix, got, tt.expectedCT)
+ t.Errorf("detectContentType(%q) = %q, want %q", tt.filename, got, tt.expectedCT)
}
})
}
@@ -286,10 +255,6 @@ func createTestArchive(t *testing.T) []byte {
"package/lib/index.js": "module.exports = {};",
"package/lib/helper.js": "module.exports.help = () => {};",
"package/test/index.test.js": "// tests",
- "package/notes.data": "short text\n",
- "package/logo": "\x89PNG\r\n\x1a\nimage data",
- "package/page": "",
- "package/misleading.txt": "\x89PNG\r\n\x1a\nimage data",
}
for path, content := range files {
@@ -644,19 +609,25 @@ func TestHandleComparePage(t *testing.T) {
}
}
-func TestOpenArchiveDetectsExtensionlessTarGz(t *testing.T) {
- reader, err := openArchive("artifact", bytes.NewReader(createTestArchive(t)), "npm")
- if err != nil {
- t.Fatalf("openArchive failed: %v", err)
+func TestArchiveFilename(t *testing.T) {
+ tests := []struct {
+ input string
+ want string
+ }{
+ {"package.tar.gz", "package.tar.gz"},
+ {"d2e2f014ccd6ec9fae8dbe6336a4164346a2a856", "d2e2f014ccd6ec9fae8dbe6336a4164346a2a856.zip"},
+ {"file.zip", "file.zip"},
+ {"archive.tgz", "archive.tgz"},
+ {"noext", "noext.zip"},
}
- defer func() { _ = reader.Close() }()
- files, err := reader.List()
- if err != nil {
- t.Fatalf("List failed: %v", err)
- }
- if len(files) == 0 {
- t.Fatal("expected files in extensionless archive")
+ for _, tt := range tests {
+ t.Run(tt.input, func(t *testing.T) {
+ got := archiveFilename(tt.input)
+ if got != tt.want {
+ t.Errorf("archiveFilename(%q) = %q, want %q", tt.input, got, tt.want)
+ }
+ })
}
}