Skip to content

Commit 61c708b

Browse files
committed
fix: Address all lint failures
Signed-off-by: RJ Sampson <rj.sampson@chainguard.dev>
1 parent 6709896 commit 61c708b

12 files changed

Lines changed: 103 additions & 64 deletions

File tree

pkg/build/build_implementation.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"encoding/json"
2424
"fmt"
2525
"io"
26+
"maps"
2627
"os"
2728
"path/filepath"
2829
"runtime"
@@ -191,9 +192,7 @@ func (bc *Context) buildImage(ctx context.Context) ([]apk.InstalledDiff, error)
191192
if bc.ic.Environment == nil {
192193
bc.ic.Environment = make(map[string]string)
193194
}
194-
for k, v := range env {
195-
bc.ic.Environment[k] = v
196-
}
195+
maps.Copy(bc.ic.Environment, env)
197196
}
198197
}
199198

pkg/build/types/schema.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,35 @@
3131
"additionalProperties": false,
3232
"type": "object"
3333
},
34+
"EcosystemConfig": {
35+
"properties": {
36+
"indexes": {
37+
"items": {
38+
"type": "string"
39+
},
40+
"type": "array",
41+
"description": "Indexes is a list of package index URLs (e.g., PyPI simple API URLs)."
42+
},
43+
"packages": {
44+
"items": {
45+
"type": "string"
46+
},
47+
"type": "array",
48+
"description": "Packages is a list of package specifications (e.g., \"flask==3.0.0\")."
49+
},
50+
"python_version": {
51+
"type": "string",
52+
"description": "PythonVersion overrides auto-detection of the Python version (e.g., \"3.12\")."
53+
},
54+
"venv": {
55+
"type": "string",
56+
"description": "Venv is an optional path for a virtual environment (e.g., \"/app/venv\").\nWhen set, packages are installed into the venv instead of the system site-packages,\nand VIRTUAL_ENV / PATH are set automatically."
57+
}
58+
},
59+
"additionalProperties": false,
60+
"type": "object",
61+
"description": "EcosystemConfig holds configuration for a non-APK package ecosystem (e.g., python)."
62+
},
3463
"Group": {
3564
"properties": {
3665
"groupname": {
@@ -217,6 +246,13 @@
217246
"baseimage": {
218247
"$ref": "#/$defs/BaseImageDescriptor",
219248
"description": "Optional: Base image to build on top of. Warning: Experimental."
249+
},
250+
"ecosystems": {
251+
"additionalProperties": {
252+
"$ref": "#/$defs/EcosystemConfig"
253+
},
254+
"type": "object",
255+
"description": "Optional: Non-APK ecosystem packages to install (e.g., pip packages)."
220256
}
221257
},
222258
"additionalProperties": false,

pkg/ecosystem/python/platform.go

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package python
1616

1717
import (
1818
"fmt"
19+
"slices"
1920
"strings"
2021

2122
"chainguard.dev/apko/pkg/build/types"
@@ -143,7 +144,7 @@ func isCompatibleWheel(w wheelFileParts, pythonVersion string, arch types.Archit
143144
// E.g., "py3", "cp312", "py2.py3"
144145
func isCompatiblePythonTag(tag, pythonVersion string) bool {
145146
cpTag := "cp" + strings.ReplaceAll(pythonVersion, ".", "")
146-
for _, t := range strings.Split(tag, ".") {
147+
for t := range strings.SplitSeq(tag, ".") {
147148
if t == "py3" || t == "py2.py3" || t == cpTag {
148149
return true
149150
}
@@ -157,7 +158,7 @@ func isCompatibleABI(tag, pythonVersion string) bool {
157158
return true
158159
}
159160
cpTag := "cp" + strings.ReplaceAll(pythonVersion, ".", "")
160-
for _, t := range strings.Split(tag, ".") {
161+
for t := range strings.SplitSeq(tag, ".") {
161162
if t == "abi3" || t == cpTag {
162163
return true
163164
}
@@ -171,11 +172,9 @@ func isCompatiblePlatform(tag string, arch types.Architecture) bool {
171172
return true
172173
}
173174
compatible := platformTags(arch)
174-
for _, t := range strings.Split(tag, ".") {
175-
for _, c := range compatible {
176-
if t == c {
177-
return true
178-
}
175+
for t := range strings.SplitSeq(tag, ".") {
176+
if slices.Contains(compatible, t) {
177+
return true
179178
}
180179
}
181180
return false
@@ -188,18 +187,19 @@ func wheelScore(w wheelFileParts, pythonVersion string, arch types.Architecture)
188187

189188
// Prefer exact CPython tag over generic py3
190189
cpTag := "cp" + strings.ReplaceAll(pythonVersion, ".", "")
191-
for _, t := range strings.Split(w.PythonTag, ".") {
190+
for t := range strings.SplitSeq(w.PythonTag, ".") {
192191
if t == cpTag {
193192
score += 100
194193
break
195194
}
196195
}
197196

198197
// Prefer specific ABI over none/abi3
199-
for _, t := range strings.Split(w.ABITag, ".") {
200-
if t == cpTag {
198+
for t := range strings.SplitSeq(w.ABITag, ".") {
199+
switch t {
200+
case cpTag:
201201
score += 50
202-
} else if t == "abi3" {
202+
case "abi3":
203203
score += 25
204204
}
205205
}
@@ -208,11 +208,10 @@ func wheelScore(w wheelFileParts, pythonVersion string, arch types.Architecture)
208208
if w.PlatformTag != "any" {
209209
platTags := platformTags(arch)
210210
for i, pt := range platTags {
211-
for _, t := range strings.Split(w.PlatformTag, ".") {
212-
if t == pt {
211+
for pp := range strings.SplitSeq(w.PlatformTag, ".") {
212+
if pp == pt {
213213
// More specific platforms (earlier in list) get higher scores
214214
score += 10 * (len(platTags) - i)
215-
break
216215
}
217216
}
218217
}

pkg/ecosystem/python/platform_test.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,9 @@ import (
2222

2323
func TestPlatformTags(t *testing.T) {
2424
tests := []struct {
25-
arch string
26-
wantLen int
27-
wantAny string // At least one tag should contain this
25+
arch string
26+
wantLen int
27+
wantAny string // At least one tag should contain this
2828
}{
2929
{"amd64", 5, "x86_64"},
3030
{"arm64", 3, "aarch64"},
@@ -136,11 +136,11 @@ func TestParseWheelFilename(t *testing.T) {
136136

137137
func TestIsCompatibleWheel(t *testing.T) {
138138
tests := []struct {
139-
name string
140-
wheel wheelFileParts
141-
pyVer string
142-
arch string
143-
want bool
139+
name string
140+
wheel wheelFileParts
141+
pyVer string
142+
arch string
143+
want bool
144144
}{
145145
{
146146
name: "pure python wheel is always compatible",

pkg/ecosystem/python/python.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ import (
2424

2525
"github.com/chainguard-dev/clog"
2626

27-
apkfs "chainguard.dev/apko/pkg/apk/fs"
2827
"chainguard.dev/apko/pkg/apk/auth"
28+
apkfs "chainguard.dev/apko/pkg/apk/fs"
2929
"chainguard.dev/apko/pkg/build/types"
3030
"chainguard.dev/apko/pkg/ecosystem"
3131
)
@@ -181,9 +181,9 @@ func createVenv(fsys apkfs.FullFS, venvPath, pythonVersion string) error {
181181
pythonBin := "/usr/bin/python" + pythonVersion
182182
binPath := filepath.Join(venvPath, "bin")
183183
symlinks := map[string]string{
184-
"python": pythonBin,
185-
"python3": pythonBin,
186-
"python" + pythonVersion: pythonBin,
184+
"python": pythonBin,
185+
"python3": pythonBin,
186+
"python" + pythonVersion: pythonBin,
187187
}
188188
for name, target := range symlinks {
189189
linkPath := filepath.Join(binPath, name)

pkg/ecosystem/python/python_test.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,6 @@ func TestCreateVenv(t *testing.T) {
6969
}
7070
}
7171

72-
7372
func TestInstallerRegistration(t *testing.T) {
7473
inst, ok := ecosystem.Get("python")
7574
if !ok {

pkg/ecosystem/python/resolve.go

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -131,14 +131,14 @@ func normalizeName(name string) string {
131131

132132
// pypiPackageJSON is the response from https://pypi.org/pypi/{name}/{version}/json
133133
type pypiPackageJSON struct {
134-
Info pypiInfo `json:"info"`
135-
URLs []pypiURL `json:"urls"`
134+
Info pypiInfo `json:"info"`
135+
URLs []pypiURL `json:"urls"`
136136
}
137137

138138
type pypiInfo struct {
139-
Name string `json:"name"`
140-
Version string `json:"version"`
141-
RequiresDist []string `json:"requires_dist"`
139+
Name string `json:"name"`
140+
Version string `json:"version"`
141+
RequiresDist []string `json:"requires_dist"`
142142
}
143143

144144
type pypiURL struct {
@@ -300,7 +300,7 @@ func resolveJSONVersion(ctx context.Context, normalizedName, originalName, versi
300300
}
301301

302302
// Parse dependencies from requires_dist
303-
var deps []packageSpec
303+
deps := make([]packageSpec, 0, len(pkgResp.Info.RequiresDist))
304304
for _, req := range pkgResp.Info.RequiresDist {
305305
dep := parsePackageSpec(req)
306306
if dep.Markers != "" && !evaluateMarkers(dep.Markers, nil) {
@@ -381,16 +381,16 @@ type wheelLink struct {
381381

382382
// parseSimpleIndex parses the HTML from a PEP 503 Simple Repository API response.
383383
func parseSimpleIndex(body string, baseURL string) []wheelLink {
384-
var links []wheelLink
385-
386384
// Use a regex that handles '>' inside quoted attribute values (e.g., data-requires-python=">=3.0").
387385
// The [^>]* approach breaks when attributes contain '>' characters.
388386
linkRe := regexp.MustCompile(`<a\s+(?:[^>"]*(?:"[^"]*")?)*href="([^"]*)"(?:[^>"]*(?:"[^"]*")?)*>([^<]*)</a>`)
389387
requiresPythonRe := regexp.MustCompile(`data-requires-python="([^"]*)"`)
390388
provenanceRe := regexp.MustCompile(`data-provenance="([^"]*)"`)
391389
signatureRe := regexp.MustCompile(`data-signature="([^"]*)"`)
392390

393-
for _, match := range linkRe.FindAllStringSubmatch(body, -1) {
391+
matches := linkRe.FindAllStringSubmatch(body, -1)
392+
links := make([]wheelLink, 0, len(matches))
393+
for _, match := range matches {
394394
href := match[1]
395395
filename := strings.TrimSpace(match[2])
396396

@@ -538,8 +538,8 @@ func extractDepsFromWheel(ctx context.Context, url string, a auth.Authenticator)
538538

539539
// parseRequiresDist extracts Requires-Dist entries from wheel METADATA content.
540540
func parseRequiresDist(metadata string) []packageSpec {
541-
var deps []packageSpec
542-
for _, line := range strings.Split(metadata, "\n") {
541+
deps := make([]packageSpec, 0, strings.Count(metadata, "Requires-Dist: "))
542+
for line := range strings.SplitSeq(metadata, "\n") {
543543
line = strings.TrimRight(line, "\r")
544544
if !strings.HasPrefix(line, "Requires-Dist: ") {
545545
continue
@@ -644,9 +644,7 @@ func compareVersions(a, b string) int {
644644
bParts := strings.Split(b, ".")
645645

646646
maxLen := len(aParts)
647-
if len(bParts) > maxLen {
648-
maxLen = len(bParts)
649-
}
647+
maxLen = max(maxLen, len(bParts))
650648

651649
for i := 0; i < maxLen; i++ {
652650
var aVal, bVal string

pkg/ecosystem/python/resolve_test.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"encoding/json"
2020
"net/http"
2121
"net/http/httptest"
22+
"strings"
2223
"testing"
2324

2425
"chainguard.dev/apko/pkg/build/types"
@@ -265,12 +266,13 @@ func servePyPIJSON(t *testing.T, packages map[string]pypiPackageJSON) *httptest.
265266
// Serve Simple API as fallback
266267
mux.HandleFunc("/simple/"+name+"/", func(w http.ResponseWriter, r *http.Request) {
267268
w.Header().Set("Content-Type", "text/html")
268-
html := "<html><body>\n"
269+
var b strings.Builder
270+
b.WriteString("<html><body>\n")
269271
for _, u := range pkg.URLs {
270-
html += `<a href="` + u.URL + `#sha256=` + u.Digests.SHA256 + `">` + u.Filename + "</a>\n"
272+
b.WriteString(`<a href="` + u.URL + `#sha256=` + u.Digests.SHA256 + `">` + u.Filename + "</a>\n")
271273
}
272-
html += "</body></html>"
273-
w.Write([]byte(html))
274+
b.WriteString("</body></html>")
275+
w.Write([]byte(b.String()))
274276
})
275277
}
276278
return httptest.NewServer(mux)

pkg/ecosystem/python/sbom.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -68,13 +68,13 @@ func writePackageSBOM(fsys apkfs.FullFS, sitePackagesPath string, wheelData []by
6868

6969
// spdxDocument is a minimal SPDX 2.3 JSON document structure.
7070
type spdxDocument struct {
71-
SPDXVersion string `json:"spdxVersion"`
72-
DataLicense string `json:"dataLicense"`
73-
SPDXID string `json:"SPDXID"`
74-
Name string `json:"name"`
75-
Namespace string `json:"documentNamespace"`
76-
CreationInfo spdxCreationInfo `json:"creationInfo"`
77-
Packages []spdxPackage `json:"packages"`
71+
SPDXVersion string `json:"spdxVersion"`
72+
DataLicense string `json:"dataLicense"`
73+
SPDXID string `json:"SPDXID"`
74+
Name string `json:"name"`
75+
Namespace string `json:"documentNamespace"`
76+
CreationInfo spdxCreationInfo `json:"creationInfo"`
77+
Packages []spdxPackage `json:"packages"`
7878
}
7979

8080
type spdxCreationInfo struct {

pkg/ecosystem/python/wheel.go

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"bytes"
2020
"crypto/sha256"
2121
"encoding/hex"
22+
"errors"
2223
"fmt"
2324
"io"
2425
"path/filepath"
@@ -35,8 +36,13 @@ func extractWheel(fsys apkfs.FullFS, wheelData []byte, sitePackagesPath string)
3536
return fmt.Errorf("opening wheel as zip: %w", err)
3637
}
3738

39+
cleanBase := filepath.Clean(sitePackagesPath) + string(filepath.Separator)
3840
for _, f := range reader.File {
39-
targetPath := filepath.Join(sitePackagesPath, f.Name)
41+
// G305: Protect against zip slip / path traversal.
42+
targetPath := filepath.Join(sitePackagesPath, filepath.Clean(f.Name))
43+
if !strings.HasPrefix(targetPath, cleanBase) {
44+
return errors.New("illegal file path in wheel archive: " + f.Name)
45+
}
4046

4147
if f.FileInfo().IsDir() {
4248
if err := fsys.MkdirAll(targetPath, 0755); err != nil {

0 commit comments

Comments
 (0)