mirror of
https://github.com/anchore/syft.git
synced 2026-10-11 21:57:22 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
33419c8f39 | ||
|
|
6790272518 | ||
|
|
740beb6dc4 | ||
|
|
3e41abadc4 | ||
|
|
0a050b0e27 | ||
|
|
fc8bd6c9d8 | ||
|
|
7e713d106e | ||
|
|
1f5ac47990 | ||
|
|
28e38c29f3 | ||
|
|
546c872c9d | ||
|
|
b0a8de7bf6 | ||
|
|
d9489c2230 |
+1
-1
@@ -24,7 +24,7 @@ tools:
|
||||
# used to parse JSON/YAML annotations on the fixture cache image
|
||||
- name: yq
|
||||
version:
|
||||
want: v4.53.6
|
||||
want: v4.54.1
|
||||
method: github-release
|
||||
with:
|
||||
repo: mikefarah/yq
|
||||
|
||||
@@ -30,7 +30,7 @@ runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup go + go-make tooling
|
||||
uses: anchore/go-make/.github/actions/setup@fa5421b3bf24c9d18f11a736c5eb4172f2b7d683 # v0.8.1
|
||||
uses: anchore/go-make/.github/actions/setup@27a9f35f41b5fe86d68815844efd2ba816e9ba6b # v0.9.1
|
||||
with:
|
||||
go-version: ${{ inputs.go-version }}
|
||||
cache-key-prefix: ${{ inputs.cache-key-prefix }}
|
||||
|
||||
@@ -118,7 +118,7 @@ jobs:
|
||||
# for updating brew formula in anchore/homebrew-syft
|
||||
GITHUB_BREW_TOKEN: ${{ secrets.ANCHOREOPS_GITHUB_OSS_WRITE_TOKEN }}
|
||||
|
||||
- uses: anchore/sbom-action@3ad7283483fc7af8ff2b4ea19663c2d5ca935e26 #v0.24.2
|
||||
- uses: anchore/sbom-action@66cbf4bc1f1c0d2edc94016e65bc221b6bb0ad6c #v0.24.3
|
||||
continue-on-error: true
|
||||
with:
|
||||
file: go.mod
|
||||
|
||||
@@ -99,7 +99,7 @@ jobs:
|
||||
# go-make's setup only (not ./.github/actions/bootstrap): binny ships no windows release, so binny tools
|
||||
# are unavailable here (`make unit` skips the fixture refresh on windows)
|
||||
- name: Setup go + go-make tooling
|
||||
uses: anchore/go-make/.github/actions/setup@fa5421b3bf24c9d18f11a736c5eb4172f2b7d683 # v0.8.1
|
||||
uses: anchore/go-make/.github/actions/setup@27a9f35f41b5fe86d68815844efd2ba816e9ba6b # v0.9.1
|
||||
with:
|
||||
# keep in sync with the go-version default in ./.github/actions/bootstrap
|
||||
go-version: "1.26.2"
|
||||
@@ -195,7 +195,7 @@ jobs:
|
||||
|
||||
# go-make's setup only (not ./.github/actions/bootstrap): the test only needs make + the snapshot binary
|
||||
- name: Setup go + go-make tooling
|
||||
uses: anchore/go-make/.github/actions/setup@fa5421b3bf24c9d18f11a736c5eb4172f2b7d683 # v0.8.1
|
||||
uses: anchore/go-make/.github/actions/setup@27a9f35f41b5fe86d68815844efd2ba816e9ba6b # v0.9.1
|
||||
with:
|
||||
# keep in sync with the go-version default in ./.github/actions/bootstrap
|
||||
go-version: "1.26.2"
|
||||
@@ -228,7 +228,7 @@ jobs:
|
||||
|
||||
# go-make's setup only (not ./.github/actions/bootstrap): the test only needs make + the snapshot binary
|
||||
- name: Setup go + go-make tooling
|
||||
uses: anchore/go-make/.github/actions/setup@fa5421b3bf24c9d18f11a736c5eb4172f2b7d683 # v0.8.1
|
||||
uses: anchore/go-make/.github/actions/setup@27a9f35f41b5fe86d68815844efd2ba816e9ba6b # v0.9.1
|
||||
with:
|
||||
# keep in sync with the go-version default in ./.github/actions/bootstrap
|
||||
go-version: "1.26.2"
|
||||
|
||||
+4
-4
@@ -1,12 +1,12 @@
|
||||
module github.com/anchore/syft/.make
|
||||
|
||||
go 1.25.8
|
||||
go 1.26.0
|
||||
|
||||
require github.com/anchore/go-make v0.8.1
|
||||
require github.com/anchore/go-make v0.9.1
|
||||
|
||||
require (
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
golang.org/x/mod v0.40.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/mod v0.41.0 // indirect
|
||||
golang.org/x/sys v0.48.0 // indirect
|
||||
)
|
||||
|
||||
+6
-6
@@ -1,10 +1,10 @@
|
||||
github.com/anchore/go-make v0.8.1 h1:GAhPXR+9qVQhFB1pfVlHFOzxAt3dTDTUIA7Tp7nx9tg=
|
||||
github.com/anchore/go-make v0.8.1/go.mod h1:xALbVsPsDoJnJxdkjPlxOLZV7GbAdT5Pv3gd0kP1Lz0=
|
||||
github.com/anchore/go-make v0.9.1 h1:OnC830XJEhdHUPhR8jLpnjmLTco9UThNZfHpLefqd94=
|
||||
github.com/anchore/go-make v0.9.1/go.mod h1:1KhqPt0STAHLVEngwrXxYsuFftD62Nx497rNujwo8IY=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs=
|
||||
github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
|
||||
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
|
||||
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c=
|
||||
golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o=
|
||||
golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo=
|
||||
golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/syft/internal/redact"
|
||||
)
|
||||
|
||||
func TestCommandCanRunMultipleTimes(t *testing.T) {
|
||||
// https://github.com/anchore/syft/issues/2285
|
||||
t.Cleanup(redact.Reset)
|
||||
|
||||
for i := 0; i < 2; i++ {
|
||||
_, cmd := create(clio.Identification{Name: "syft"}, io.Discard)
|
||||
cmd.SetArgs([]string{"cataloger", "list", "-o", "json"})
|
||||
require.NoError(t, cmd.Execute())
|
||||
|
||||
// the store is released at the end of each run so the next one can set its own
|
||||
require.Nil(t, redact.Get())
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,8 @@ func AppClioSetupConfig(id clio.Identification, out io.Writer) *clio.SetupConfig
|
||||
stereoscope.SetBus(state.Bus)
|
||||
bus.Set(state.Bus)
|
||||
|
||||
// the redact store is process-global and only cleared when a run ends (see the post-run below), so
|
||||
// setting it while another run is still in flight panics rather than splitting secrets across stores.
|
||||
redact.Set(state.RedactStore)
|
||||
|
||||
log.Set(state.Logger)
|
||||
@@ -48,8 +50,14 @@ func AppClioSetupConfig(id clio.Identification, out io.Writer) *clio.SetupConfig
|
||||
return nil
|
||||
},
|
||||
).
|
||||
WithPostRuns(func(_ *clio.State, _ error) {
|
||||
WithPostRuns(func(state *clio.State, _ error) {
|
||||
stereoscope.Cleanup() //nolint:staticcheck // we don't have access to the image object here
|
||||
|
||||
// release the redact store now that nothing in this run can report anymore, so a later run in the same
|
||||
// process (e.g. syft embedded as a library) can set its own. Only release the store this run set.
|
||||
if redact.Get() == state.RedactStore {
|
||||
redact.Reset()
|
||||
}
|
||||
})
|
||||
return clioCfg
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/go-logger/adapter/discard"
|
||||
gologgerredact "github.com/anchore/go-logger/adapter/redact"
|
||||
"github.com/anchore/syft/internal/redact"
|
||||
)
|
||||
|
||||
func TestAppClioSetupConfigInitializerPanicsWhileRunInFlight(t *testing.T) {
|
||||
t.Cleanup(redact.Reset)
|
||||
|
||||
cfg := AppClioSetupConfig(clio.Identification{Name: "syft"}, io.Discard)
|
||||
require.Len(t, cfg.Initializers, 1)
|
||||
|
||||
newState := func() *clio.State {
|
||||
return &clio.State{Logger: discard.New(), RedactStore: gologgerredact.NewStore()}
|
||||
}
|
||||
|
||||
require.NoError(t, cfg.Initializers[0](newState()))
|
||||
|
||||
// a second run starting before the first has finished must not silently replace the store, otherwise
|
||||
// secrets added to the first store would no longer be redacted
|
||||
require.PanicsWithValue(t, "replace existing redaction store (probably unintentional)", func() {
|
||||
_ = cfg.Initializers[0](newState())
|
||||
})
|
||||
}
|
||||
@@ -41,7 +41,7 @@ require (
|
||||
github.com/facebookincubator/nvdtools v0.1.5
|
||||
github.com/github/go-spdx/v2 v2.7.0
|
||||
github.com/gkampitakis/go-snaps v0.5.23
|
||||
github.com/go-git/go-billy/v5 v5.9.1
|
||||
github.com/go-git/go-billy/v5 v5.9.2
|
||||
github.com/go-git/go-git/v5 v5.19.2
|
||||
github.com/go-test/deep v1.1.1
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0
|
||||
@@ -52,7 +52,7 @@ require (
|
||||
github.com/google/licensecheck v0.3.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gookit/color v1.6.1
|
||||
github.com/gpustack/gguf-parser-go v0.26.3
|
||||
github.com/gpustack/gguf-parser-go v0.26.4
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2
|
||||
github.com/hashicorp/go-getter v1.8.9
|
||||
github.com/hashicorp/go-multierror v1.1.1
|
||||
@@ -97,11 +97,11 @@ require (
|
||||
golang.org/x/net v0.59.0
|
||||
golang.org/x/time v0.16.0
|
||||
golang.org/x/tools v0.50.0
|
||||
modernc.org/sqlite v1.59.0
|
||||
modernc.org/sqlite v1.60.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/pb33f/ordered-map/v2 v2.3.1
|
||||
github.com/pb33f/ordered-map/v2 v2.3.2
|
||||
github.com/tailscale/hujson v0.0.0-20260302212456-ecc657c15afd
|
||||
howett.net/plist v1.0.1
|
||||
)
|
||||
@@ -265,6 +265,7 @@ require (
|
||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||
github.com/opencontainers/runtime-spec v1.3.0 // indirect
|
||||
github.com/opencontainers/selinux v1.15.1 // indirect
|
||||
github.com/pb33f/go-yaml v0.1.0 // indirect
|
||||
github.com/pborman/indent v1.2.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.4.3 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.26 // indirect
|
||||
@@ -328,7 +329,6 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.46.0 // indirect
|
||||
go.podman.io/image/v5 v5.41.2 // indirect
|
||||
go.podman.io/storage v1.64.1 // indirect
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
|
||||
golang.org/x/crypto v0.57.0 // indirect
|
||||
golang.org/x/oauth2 v0.36.0 // indirect
|
||||
@@ -345,7 +345,7 @@ require (
|
||||
google.golang.org/grpc v1.83.2 // indirect
|
||||
google.golang.org/protobuf v1.36.12 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
modernc.org/libc v1.75.7 // indirect
|
||||
modernc.org/libc v1.77.1 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.12.1 // indirect
|
||||
)
|
||||
|
||||
@@ -434,8 +434,8 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
|
||||
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
|
||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
|
||||
github.com/go-git/go-billy/v5 v5.9.1 h1:8U73XiOTfINdItHVa6z4Gv7ToObcZ6grkqQbLryLCdA=
|
||||
github.com/go-git/go-billy/v5 v5.9.1/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0=
|
||||
github.com/go-git/go-billy/v5 v5.9.2 h1:OXFSRyz4g20upsGDJgQG9Bak1l/ZEv8GHVYB52O71sE=
|
||||
github.com/go-git/go-billy/v5 v5.9.2/go.mod h1:ExsU+jcGwXTBOnyilvAnEM1wug1IxHr4yP2ZXsNRtV0=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
|
||||
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
|
||||
github.com/go-git/go-git/v5 v5.19.2 h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
|
||||
@@ -579,8 +579,8 @@ github.com/gookit/color v1.6.1 h1:KoTnDxJPRgrL0SoX0f8rCFg2zI0t4E3GZZBMo2nN8LU=
|
||||
github.com/gookit/color v1.6.1/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gpustack/gguf-parser-go v0.26.3 h1:F63PlUPIW56HGU5k4Crv0JAG9ojtz/TuwzgMYlb82Ec=
|
||||
github.com/gpustack/gguf-parser-go v0.26.3/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0=
|
||||
github.com/gpustack/gguf-parser-go v0.26.4 h1:B9CRrQBcIB7I/MJFZvzPZ+e+cbc1xBzQJcmpi9l/mKU=
|
||||
github.com/gpustack/gguf-parser-go v0.26.4/go.mod h1:y4TwTtDqFWTK+xvprOjRUh+dowgU2TKCX37vRKvGiZ0=
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74 h1:mymLUKThnV9wFvogOK8NnsMP9/vlhnjXY98gr2QIGW8=
|
||||
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.74/go.mod h1:Bh9qYL8ehmDxSg14Tk8oxFCP90XHfs6NxV1D84884xA=
|
||||
@@ -808,8 +808,10 @@ github.com/opencontainers/selinux v1.15.1/go.mod h1:LenyElirjUHszfxrjuFqC85HIeXZ
|
||||
github.com/orisano/pixelmatch v0.0.0-20220722002657-fb0b55479cde/go.mod h1:nZgzbfBr3hhjoZnS66nKrHmduYNpc34ny7RK4z5/HM0=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pb33f/ordered-map/v2 v2.3.1 h1:5319HDO0aw4DA4gzi+zv4FXU9UlSs3xGZ40wcP1nBjY=
|
||||
github.com/pb33f/ordered-map/v2 v2.3.1/go.mod h1:qxFQgd0PkVUtOMCkTapqotNgzRhMPL7VvaHKbd1HnmQ=
|
||||
github.com/pb33f/go-yaml v0.1.0 h1:zZ6Gg+ATbumXlXZNLMHIe7+zTkVqe20lx+TSYRrXEuY=
|
||||
github.com/pb33f/go-yaml v0.1.0/go.mod h1:QvWdkHP5VjFYAeHMQUkaPQqREaQnBNOXmWzAjNrnSdo=
|
||||
github.com/pb33f/ordered-map/v2 v2.3.2 h1:wDyaZ2Pv9QLh64X4utCeD5Zoi9nIv2aW3lwv172O5PQ=
|
||||
github.com/pb33f/ordered-map/v2 v2.3.2/go.mod h1:1OhFrXu3OYw3kM+FXF+Ug3ugmmZ9LE8z0JfQMLvtV0w=
|
||||
github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM=
|
||||
github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw=
|
||||
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||
@@ -1084,8 +1086,6 @@ go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
|
||||
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -1610,10 +1610,10 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
|
||||
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
|
||||
modernc.org/cc/v4 v4.29.2 h1:h6+9ciCnPKutf4I03CvheAvDLX7+IHlqR6Iy6J+cgd8=
|
||||
modernc.org/cc/v4 v4.29.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.35.0 h1:F+TUsmw09QxLzmi3aeYYGxjAXarmZaKgj3mKQHNaA8w=
|
||||
modernc.org/ccgo/v4 v4.35.0/go.mod h1:qrVGs9S3Sr2Ztcg9ve+kTAYMp5a3YvWjo+SoN06kJ5I=
|
||||
modernc.org/cc/v4 v4.29.7 h1:q+NXGJ0bK3b4TXFYQQVr9pYETGnmwFWkrUzJnMya/Tg=
|
||||
modernc.org/cc/v4 v4.29.7/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.36.1 h1:ZNIUZAryN0UgnJwtyxrdEzcFc3yD4Cu4AzjfPXsLsIE=
|
||||
modernc.org/ccgo/v4 v4.36.1/go.mod h1:rrtGc2QkS239nYb/mQNuBMyjq3/y3ZXWbBjPoV3wqzA=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
@@ -1622,8 +1622,8 @@ modernc.org/gc/v3 v3.1.5 h1:21ldfPfRYE31Tb7B3mwAK8gy1AxP4+dKjrOQPfqakoc=
|
||||
modernc.org/gc/v3 v3.1.5/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.75.7 h1:o3DTP9/0p9pKmY2WCKQaySW6wIiZhNM7wc2lUoyhfew=
|
||||
modernc.org/libc v1.75.7/go.mod h1:bO5o2ztHxBb2rjz0PgdHN0sSMw57CgxGFLZ3Qd/QpVQ=
|
||||
modernc.org/libc v1.77.1 h1:Ct8j47QtiZ1Enj2DtFXQtUqrPCAjdCmPjtCuvrYQ0Hs=
|
||||
modernc.org/libc v1.77.1/go.mod h1:87/pZ4L6nD1zqW4nItuS12YO7hN1igAah34xjnQo/W0=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.12.1 h1:nFMiWrpStgZczNl6XI9GnIk/rWhYIyHGUaR04pGbp9g=
|
||||
@@ -1632,8 +1632,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.59.0 h1:X1es1GpqBlS/5T+vbM4HLUdaa8OtQx468DF2vrx+38A=
|
||||
modernc.org/sqlite v1.59.0/go.mod h1:+paeT2A3iPRHkQDwG7oA6Tk0zQd5woMEI8q7orfry8k=
|
||||
modernc.org/sqlite v1.60.1 h1:/blz53O951KWFOso4QQvEs/Fq6cDBKLtMVrYNSeJVKw=
|
||||
modernc.org/sqlite v1.60.1/go.mod h1:1dIoEagfDE72QytD5scH1lxARtaUgKgHC/NuApA27r0=
|
||||
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=
|
||||
|
||||
@@ -13,6 +13,14 @@ func Set(s redact.Store) {
|
||||
store = s
|
||||
}
|
||||
|
||||
// Reset clears the global redaction store. It must only be called once a command
|
||||
// run has fully finished (nothing left to Add or Apply), so that a later run can
|
||||
// Set its own store. Calling it while a run is in flight would let a second Set
|
||||
// split secrets across two stores and leak the ones in the dropped store.
|
||||
func Reset() {
|
||||
store = nil
|
||||
}
|
||||
|
||||
func Get() redact.Store {
|
||||
return store
|
||||
}
|
||||
|
||||
@@ -75,6 +75,7 @@ catalogers:
|
||||
method: glob # AUTO-GENERATED
|
||||
criteria: # AUTO-GENERATED
|
||||
- '**/*.deb'
|
||||
- '**/*.ipk'
|
||||
metadata_types: # AUTO-GENERATED
|
||||
- pkg.DpkgArchiveEntry
|
||||
package_types: # AUTO-GENERATED
|
||||
|
||||
@@ -18,8 +18,8 @@ func NewDBCataloger() pkg.Cataloger {
|
||||
WithProcessors(dependency.Processor(dbEntryDependencySpecifier))
|
||||
}
|
||||
|
||||
// NewArchiveCataloger returns a new Debian package cataloger object capable of parsing .deb archive files
|
||||
// NewArchiveCataloger returns a new Debian package cataloger object capable of parsing .deb and .ipk (opkg) archive files
|
||||
func NewArchiveCataloger() pkg.Cataloger {
|
||||
return generic.NewCataloger("deb-archive-cataloger").
|
||||
WithParserByGlobs(parseDebArchive, "**/*.deb")
|
||||
WithParserByGlobs(parseDebArchive, "**/*.deb", "**/*.ipk")
|
||||
}
|
||||
|
||||
@@ -276,6 +276,36 @@ func TestDpkgArchiveCataloger(t *testing.T) {
|
||||
},
|
||||
},
|
||||
},
|
||||
}, {
|
||||
name: "image-single-ipk",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "libz1",
|
||||
Version: "1.3.2-r0",
|
||||
FoundBy: "deb-archive-cataloger",
|
||||
Locations: file.NewLocationSet(
|
||||
file.NewLocation("/libz1_1.3.2-r0_x86-64-v3.ipk"),
|
||||
),
|
||||
Type: pkg.DebPkg,
|
||||
Metadata: pkg.DpkgArchiveEntry{
|
||||
Package: "libz1",
|
||||
Source: "zlib",
|
||||
SourceVersion: "1.3.2",
|
||||
Version: "1.3.2-r0",
|
||||
Architecture: "x86-64-v3",
|
||||
Maintainer: "Poky Maintainers <poky@lists.yoctoproject.org>",
|
||||
InstalledSize: 0,
|
||||
Description: `Zlib Compression Library
|
||||
Zlib is a general-purpose, patent-free, lossless data compression library
|
||||
which is used by many different programs.`,
|
||||
Provides: []string{"zlib"},
|
||||
Depends: []string{"musl (>= 1.2.6+git0+9fa28ece75)"},
|
||||
},
|
||||
Licenses: pkg.NewLicenseSet(
|
||||
pkg.NewLicenseFromLocationsWithContext(ctx, "Zlib"),
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,8 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/scylladb/go-set/strset"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
@@ -61,32 +63,65 @@ func newDpkgPackage(ctx context.Context, d dpkgExtractedMetadata, dbLocation fil
|
||||
return p
|
||||
}
|
||||
|
||||
// a declared License field holds a single expression, a few hundred bytes at most in practice. Anything
|
||||
// much larger only feeds the expression parser (which recurses on parentheses) attacker-sized input.
|
||||
const maxDeclaredLicenseLen = 4096
|
||||
|
||||
var (
|
||||
// opkg/ipkg (Yocto via bitbake) declare licenses with "&" for AND and "|" for OR
|
||||
bitbakeLicenseOperators = strings.NewReplacer("&", " AND ", "|", " OR ")
|
||||
tightenParens = strings.NewReplacer("( ", "(", " )", ")")
|
||||
licenseListSeparators = strings.NewReplacer("(", " ", ")", " ")
|
||||
)
|
||||
|
||||
// extractDeclaredLicenses converts a License field from the status DB into a license set. Returns nil
|
||||
// for empty input so standard dpkg entries (which never declare License inline) incur no allocation.
|
||||
// Mirrors the alpine cataloger's approach: keep the value whole if it parses as a valid SPDX expression,
|
||||
// otherwise split on whitespace to handle space-separated lists.
|
||||
func extractDeclaredLicenses(ctx context.Context, raw string, dbLocation file.Location) []pkg.License {
|
||||
values := declaredLicenseValues(raw)
|
||||
if len(values) == 0 {
|
||||
return nil
|
||||
}
|
||||
return pkg.NewLicensesFromLocationWithContext(ctx, dbLocation, values...)
|
||||
}
|
||||
|
||||
// declaredLicenseValues converts an inline License field (opkg status DB or an archive control file) into
|
||||
// license values. Bitbake operators are rewritten to SPDX ones and the value is kept whole if it then parses
|
||||
// as a valid SPDX expression (mirroring the alpine cataloger), otherwise it is treated as a list of license
|
||||
// names with operators and grouping dropped.
|
||||
func declaredLicenseValues(raw string) []string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return nil
|
||||
}
|
||||
licenseStrings := []string{raw}
|
||||
if _, err := license.ParseExpression(raw); err != nil {
|
||||
licenseStrings = strings.Fields(raw)
|
||||
if len(raw) > maxDeclaredLicenseLen {
|
||||
log.WithFields("length", len(raw)).Debug("ignoring oversized declared license field")
|
||||
return nil
|
||||
}
|
||||
return pkg.NewLicensesFromLocationWithContext(ctx, dbLocation, licenseStrings...)
|
||||
|
||||
expr := tightenParens.Replace(strings.Join(strings.Fields(bitbakeLicenseOperators.Replace(raw)), " "))
|
||||
if _, err := license.ParseExpression(expr); err == nil {
|
||||
return []string{expr}
|
||||
}
|
||||
|
||||
var values []string
|
||||
seen := strset.New()
|
||||
for _, field := range strings.Fields(licenseListSeparators.Replace(expr)) {
|
||||
if field == "AND" || field == "OR" || seen.Has(field) {
|
||||
continue
|
||||
}
|
||||
seen.Add(field)
|
||||
values = append(values, field)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func newDebArchivePackage(ctx context.Context, location file.Location, metadata pkg.DpkgArchiveEntry, licenseStrings []string) pkg.Package {
|
||||
p := pkg.Package{
|
||||
Name: metadata.Package,
|
||||
Version: metadata.Version,
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, licenseStrings...)...),
|
||||
Type: pkg.DebPkg,
|
||||
PURL: packageURL(
|
||||
pkg.DpkgDBEntry(metadata),
|
||||
// we don't know the distro information, but since this is a deb file then we can reasonably assume it is a debian-based distro
|
||||
&linux.Release{IDLike: []string{"debian"}},
|
||||
),
|
||||
Name: metadata.Package,
|
||||
Version: metadata.Version,
|
||||
Licenses: pkg.NewLicenseSet(pkg.NewLicensesFromValuesWithContext(ctx, licenseStrings...)...),
|
||||
Type: pkg.DebPkg,
|
||||
PURL: packageURL(pkg.DpkgDBEntry(metadata), archiveDistro(location)),
|
||||
Metadata: metadata,
|
||||
Locations: file.NewLocationSet(location.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
|
||||
}
|
||||
@@ -95,6 +130,17 @@ func newDebArchivePackage(ctx context.Context, location file.Location, metadata
|
||||
return p
|
||||
}
|
||||
|
||||
// archiveDistro guesses the distro an archive was built for. We don't know the distro information, but a .deb
|
||||
// can reasonably be assumed to target a debian-based distro. An .ipk targets opkg-based distros (Yocto,
|
||||
// OpenWrt) which have no purl type, so it gets no distro and therefore no PURL, the same as packages found in
|
||||
// an opkg status DB.
|
||||
func archiveDistro(location file.Location) *linux.Release {
|
||||
if strings.EqualFold(path.Ext(location.Path()), ".ipk") {
|
||||
return nil
|
||||
}
|
||||
return &linux.Release{IDLike: []string{"debian"}}
|
||||
}
|
||||
|
||||
// PackageURL returns the PURL for the specific Debian package (see https://github.com/package-url/purl-spec)
|
||||
func packageURL(m pkg.DpkgDBEntry, distro *linux.Release) string {
|
||||
if distro == nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package debian
|
||||
import (
|
||||
"context"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
@@ -145,6 +146,36 @@ func Test_extractDeclaredLicenses(t *testing.T) {
|
||||
raw: "GPL-2.0 BSD-3-Clause",
|
||||
expected: []string{"GPL-2.0", "BSD-3-Clause"},
|
||||
},
|
||||
{
|
||||
name: "bitbake operators become an SPDX expression",
|
||||
raw: "MIT & (BSD-3-Clause | Apache-2.0) & ( GPL-2.0-or-later )",
|
||||
expected: []string{"MIT AND (BSD-3-Clause OR Apache-2.0) AND (GPL-2.0-or-later)"},
|
||||
},
|
||||
{
|
||||
name: "bitbake grouping is preserved rather than split",
|
||||
raw: "(MIT & BSD-3-Clause) | Apache-2.0",
|
||||
expected: []string{"(MIT AND BSD-3-Clause) OR Apache-2.0"},
|
||||
},
|
||||
{
|
||||
name: "parenthesized alternatives are preserved",
|
||||
raw: "(MIT) | (Apache-2.0)",
|
||||
expected: []string{"(MIT) OR (Apache-2.0)"},
|
||||
},
|
||||
{
|
||||
name: "non-SPDX names drop operators and grouping",
|
||||
raw: "GPLv2+ & (LGPLv2.1 | BSD) & GPLv2+",
|
||||
expected: []string{"GPLv2+", "LGPLv2.1", "BSD"},
|
||||
},
|
||||
{
|
||||
name: "oversized field is ignored",
|
||||
raw: strings.Repeat("MIT & ", maxDeclaredLicenseLen),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "deeply nested parens are ignored",
|
||||
raw: strings.Repeat("(", maxDeclaredLicenseLen) + "MIT" + strings.Repeat(")", maxDeclaredLicenseLen),
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -2,6 +2,7 @@ package debian
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -23,18 +24,27 @@ import (
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
)
|
||||
|
||||
// parseDebArchive parses a Debian package archive (.deb) file and returns the packages it contains.
|
||||
// A .deb file is an ar archive containing three main files:
|
||||
// - debian-binary: Version of the .deb format (usually "2.0")
|
||||
// parseDebArchive parses a Debian package archive (.deb) or opkg package (.ipk) file and returns the packages it
|
||||
// contains. Both are ar archives containing three main files:
|
||||
// - debian-binary: Version of the archive format (usually "2.0")
|
||||
// - control.tar.gz/xz/zst: Contains package metadata (control file, md5sums, conffiles)
|
||||
// - data.tar.gz/xz/zst: Contains the actual files to be installed (not processed by this cataloger)
|
||||
//
|
||||
// This function extracts and processes the control information to create package metadata.
|
||||
// This function extracts and processes the control information to create package metadata. Licenses come
|
||||
// from copyright files in the data tar, falling back to the control file's License field (common for .ipk).
|
||||
// Only ar-framed .ipk files are supported; the older gzipped-tar framing (e.g. OpenWrt's ipkg-build) is
|
||||
// rejected as an invalid archive.
|
||||
func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
arReader := ar.NewReader(reader)
|
||||
validatedReader, err := newValidatedArReader(reader)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("invalid ar archive: %w", err)
|
||||
}
|
||||
|
||||
arReader := ar.NewReader(validatedReader)
|
||||
|
||||
var metadata *pkg.DpkgArchiveEntry
|
||||
var licenses []string
|
||||
var ctrlLicenses []string
|
||||
var unknownErr error
|
||||
var sawControl, sawData bool
|
||||
for {
|
||||
@@ -56,7 +66,7 @@ func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen
|
||||
if err != nil {
|
||||
return nil, nil, unknown.New(reader.Location, fmt.Errorf("failed to decompress control.tar.* file: %w", err))
|
||||
}
|
||||
metadata, err = processControlTar(dcReader)
|
||||
metadata, ctrlLicenses, err = processControlTar(dcReader)
|
||||
switch {
|
||||
case err != nil && metadata == nil:
|
||||
return nil, nil, unknown.New(reader.Location, fmt.Errorf("failed to process control.tar.* file: %w", err))
|
||||
@@ -87,6 +97,10 @@ func parseDebArchive(ctx context.Context, _ file.Resolver, _ *generic.Environmen
|
||||
return nil, nil, unknown.New(reader.Location, fmt.Errorf("no application found described in .dpkg archive"))
|
||||
}
|
||||
|
||||
if len(licenses) == 0 && len(ctrlLicenses) > 0 {
|
||||
licenses = ctrlLicenses
|
||||
}
|
||||
|
||||
// a partial parse still yields a usable package, so report what went wrong alongside it rather than
|
||||
// dropping either one
|
||||
return []pkg.Package{
|
||||
@@ -128,10 +142,10 @@ func processDataTar(dcReader io.ReadCloser) ([]string, error) {
|
||||
}
|
||||
|
||||
// processControlTar always returns whatever metadata it managed to parse, even alongside a non-nil
|
||||
// error. A non-nil error with non-nil metadata means the package is usable but incomplete (e.g. a
|
||||
// clipped file listing); nil metadata means nothing usable was found and the caller should treat it
|
||||
// as fatal.
|
||||
func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
|
||||
// error, along with any licenses declared in the control file's License field. A non-nil error with
|
||||
// non-nil metadata means the package is usable but incomplete (e.g. a clipped file listing); nil
|
||||
// metadata means nothing usable was found and the caller should treat it as fatal.
|
||||
func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, []string, error) {
|
||||
defer internal.CloseAndLogError(dcReader, "")
|
||||
|
||||
tarReader := tar.NewReader(dcReader)
|
||||
@@ -140,6 +154,7 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
|
||||
var files []pkg.DpkgFileRecord
|
||||
var confFileRecords []pkg.DpkgFileRecord
|
||||
var listingErr error
|
||||
var licenses []string
|
||||
|
||||
for {
|
||||
header, err := tarReader.Next()
|
||||
@@ -147,7 +162,7 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return metadata, fmt.Errorf("failed to read control tar: %w", err)
|
||||
return metadata, nil, fmt.Errorf("failed to read control tar: %w", err)
|
||||
}
|
||||
|
||||
switch filepath.Base(header.Name) {
|
||||
@@ -155,12 +170,13 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
|
||||
// parseDpkgStatus already streams via bufio.Reader
|
||||
entries, err := parseDpkgStatus(tarReader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse control file: %w", err)
|
||||
return nil, nil, fmt.Errorf("failed to parse control file: %w", err)
|
||||
}
|
||||
if len(entries) == 0 {
|
||||
return nil, fmt.Errorf("no package entries found in control file")
|
||||
return nil, nil, fmt.Errorf("no package entries found in control file")
|
||||
}
|
||||
entry := pkg.DpkgArchiveEntry(entries[0].toDpkgEntry())
|
||||
licenses = declaredLicenseValues(entries[0].License)
|
||||
metadata = &entry
|
||||
case "md5sums":
|
||||
// parseDpkgMD5Info streams via bufio.Scanner and reports its own clipping/scan errors
|
||||
@@ -176,7 +192,7 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
|
||||
}
|
||||
|
||||
if metadata == nil {
|
||||
return nil, fmt.Errorf("control file not found in archive")
|
||||
return nil, nil, fmt.Errorf("control file not found in archive")
|
||||
}
|
||||
|
||||
if len(confFileRecords) > 0 && len(files) > 0 {
|
||||
@@ -195,7 +211,22 @@ func processControlTar(dcReader io.ReadCloser) (*pkg.DpkgArchiveEntry, error) {
|
||||
|
||||
// a clipped or unreadable file listing leaves the package usable, so hand both back and let the
|
||||
// caller decide
|
||||
return metadata, listingErr
|
||||
return metadata, licenses, listingErr
|
||||
}
|
||||
|
||||
// newValidatedArReader checks for the ar global header before handing the stream to the ar reader, which
|
||||
// otherwise skips those bytes unchecked and reads garbage headers from non-ar input.
|
||||
func newValidatedArReader(reader io.ReadCloser) (io.ReadCloser, error) {
|
||||
prefix := make([]byte, len(ar.GLOBAL_HEADER))
|
||||
if _, err := io.ReadFull(reader, prefix); err != nil {
|
||||
return nil, fmt.Errorf("failed to read ar header: %w", err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(prefix, []byte(ar.GLOBAL_HEADER)) {
|
||||
return nil, fmt.Errorf("expected ar header %q, got %q", string(ar.GLOBAL_HEADER), string(prefix))
|
||||
}
|
||||
|
||||
return io.NopCloser(io.MultiReader(bytes.NewReader(prefix), reader)), nil
|
||||
}
|
||||
|
||||
// a .deb's member sizes are bounded by the archive, but what they decompress to is not: xz and zstd
|
||||
|
||||
@@ -26,12 +26,13 @@ import (
|
||||
func TestProcessControlTar(t *testing.T) {
|
||||
tarBytes := createTestTarWithControlFiles(t)
|
||||
|
||||
metadata, err := processControlTar(io.NopCloser(bytes.NewReader(tarBytes)))
|
||||
metadata, licenses, err := processControlTar(io.NopCloser(bytes.NewReader(tarBytes)))
|
||||
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, metadata)
|
||||
|
||||
assert.Equal(t, "test-package", metadata.Package)
|
||||
assert.Equal(t, []string{"MIT"}, licenses)
|
||||
assert.Equal(t, "1.0.0", metadata.Version)
|
||||
|
||||
// md5sums should have been parsed into file records
|
||||
@@ -43,6 +44,14 @@ func TestProcessControlTar(t *testing.T) {
|
||||
assert.True(t, metadata.Files[0].IsConfigFile, "file listed in conffiles should be marked as config")
|
||||
}
|
||||
|
||||
func TestParseDebArchive_RejectsInvalidArchiveHeader(t *testing.T) {
|
||||
reader := file.NewLocationReadCloser(file.NewLocation("test.deb"), io.NopCloser(bytes.NewReader([]byte("not-an-ar-archive"))))
|
||||
|
||||
_, _, err := parseDebArchive(context.Background(), nil, nil, reader)
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "ar archive")
|
||||
}
|
||||
|
||||
func TestProcessControlTar_ConfigFileMarking(t *testing.T) {
|
||||
// Create a tar where conffiles lists paths that overlap with md5sums entries
|
||||
var buf bytes.Buffer
|
||||
@@ -61,9 +70,10 @@ func TestProcessControlTar_ConfigFileMarking(t *testing.T) {
|
||||
|
||||
require.NoError(t, tw.Close())
|
||||
|
||||
metadata, err := processControlTar(io.NopCloser(bytes.NewReader(buf.Bytes())))
|
||||
metadata, licenses, err := processControlTar(io.NopCloser(bytes.NewReader(buf.Bytes())))
|
||||
require.NoError(t, err)
|
||||
require.Len(t, metadata.Files, 3)
|
||||
assert.Empty(t, licenses)
|
||||
|
||||
assert.True(t, metadata.Files[0].IsConfigFile, "first file should be marked as config file")
|
||||
assert.True(t, metadata.Files[1].IsConfigFile, "second file should be marked as config file")
|
||||
@@ -75,7 +85,7 @@ func createTestTarWithControlFiles(t *testing.T) []byte {
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
|
||||
controlContent := "Package: test-package\nVersion: 1.0.0\nArchitecture: all\nMaintainer: Test <test@example.com>\nDescription: Test package\n"
|
||||
controlContent := "Package: test-package\nVersion: 1.0.0\nArchitecture: all\nMaintainer: Test <test@example.com>\nDescription: Test package\nLicense: MIT\n"
|
||||
writeTarEntry(t, tw, "control", controlContent)
|
||||
|
||||
md5Content := "d41d8cd98f00b204e9800998ecf8427e usr/bin/test-command\n"
|
||||
@@ -165,7 +175,7 @@ func Test_processControlTar_rejectsABombedControlMember(t *testing.T) {
|
||||
writeTarEntry(t, tw, "md5sums", strings.Repeat("d41d8cd98f00b204e9800998ecf8427e usr/bin/x\n", 20000))
|
||||
require.NoError(t, tw.Close())
|
||||
|
||||
metadata, err := processControlTar(newBoundedReadCloser(io.NopCloser(bytes.NewReader(tarBuf.Bytes())), 4096))
|
||||
metadata, _, err := processControlTar(newBoundedReadCloser(io.NopCloser(bytes.NewReader(tarBuf.Bytes())), 4096))
|
||||
|
||||
require.ErrorIs(t, err, errDecompressedTooLarge)
|
||||
require.NotNil(t, metadata, "a control tar read failure after \"control\" was parsed must not drop the package")
|
||||
@@ -181,7 +191,7 @@ func Test_processControlTar_clippedListingIsUsableButReported(t *testing.T) {
|
||||
writeTarEntry(t, tw, "md5sums", strings.Repeat("d41d8cd98f00b204e9800998ecf8427e usr/bin/x\n", maxDpkgFileRecords+10))
|
||||
require.NoError(t, tw.Close())
|
||||
|
||||
metadata, err := processControlTar(io.NopCloser(bytes.NewReader(tarBuf.Bytes())))
|
||||
metadata, _, err := processControlTar(io.NopCloser(bytes.NewReader(tarBuf.Bytes())))
|
||||
|
||||
require.Error(t, err, "clipping must be reported")
|
||||
require.ErrorIs(t, err, errClippedFileListing)
|
||||
@@ -418,3 +428,18 @@ func Test_decompressionStream_failsEarlyInsteadOfBuffering(t *testing.T) {
|
||||
assert.Less(t, bounded, uint64(8*intFile.MB),
|
||||
"a bounded stream must stop at the cap, not buffer the whole member and check afterwards")
|
||||
}
|
||||
|
||||
func TestProcessControlTar_DeclaredLicense(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
tw := tar.NewWriter(&buf)
|
||||
|
||||
controlContent := "Package: test-package\nVersion: 1.0.0\nArchitecture: all\nLicense: MIT & (BSD-3-Clause | Apache-2.0) & ( GPL-2.0-or-later )\n"
|
||||
writeTarEntry(t, tw, "control", controlContent)
|
||||
|
||||
require.NoError(t, tw.Close())
|
||||
|
||||
metadata, licenses, err := processControlTar(io.NopCloser(bytes.NewReader(buf.Bytes())))
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, metadata)
|
||||
assert.Equal(t, []string{"MIT AND (BSD-3-Clause OR Apache-2.0) AND (GPL-2.0-or-later)"}, licenses)
|
||||
}
|
||||
|
||||
@@ -183,9 +183,11 @@ func parseDpkgStatusEntry(reader *bufio.Reader) (*dpkgExtractedMetadata, error)
|
||||
}
|
||||
|
||||
sourceName, sourceVersion := extractSourceVersion(raw.Source)
|
||||
if sourceName != "" {
|
||||
raw.Source = sourceName
|
||||
}
|
||||
if sourceVersion != "" {
|
||||
raw.SourceVersion = sourceVersion
|
||||
raw.Source = sourceName
|
||||
}
|
||||
|
||||
if raw.Package == "" {
|
||||
@@ -263,8 +265,14 @@ func extractAllFields(reader *bufio.Reader) (map[string]any, error) {
|
||||
}
|
||||
|
||||
// If the source entry string is of the form "<name> (<version>)" then parse and return the components, if
|
||||
// of the "<name>" form, then return name and nil
|
||||
// of the "<name>" form, then return name and nil. Yocto (opkg/ipk) instead writes the bitbake recipe filename
|
||||
// ("<name>_<version>.bb"), which is split the same way.
|
||||
func extractSourceVersion(source string) (string, string) {
|
||||
if recipe, ok := strings.CutSuffix(source, ".bb"); ok && !strings.ContainsAny(recipe, " (") {
|
||||
// bitbake reserves "_" as the recipe name / version separator
|
||||
name, version, _ := strings.Cut(recipe, "_")
|
||||
return name, version
|
||||
}
|
||||
// special handling for the Source field since it has formatted data
|
||||
match := internal.MatchNamedCaptureGroups(sourceRegexp, source)
|
||||
return match["name"], match["version"]
|
||||
|
||||
@@ -366,6 +366,16 @@ func TestSourceVersionExtract(t *testing.T) {
|
||||
input: "",
|
||||
expected: []string{"", ""},
|
||||
},
|
||||
{
|
||||
name: "bitbake recipe with version",
|
||||
input: "zlib_1.3.2.bb",
|
||||
expected: []string{"zlib", "1.3.2"},
|
||||
},
|
||||
{
|
||||
name: "bitbake recipe without version",
|
||||
input: "packagegroup-core-boot.bb",
|
||||
expected: []string{"packagegroup-core-boot", ""},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
# assembles a Yocto-style .ipk the same way bitbake's package_ipk does (opkg-build: a GNU ar archive of
|
||||
# debian-binary, control.tar.gz and data.tar.zst) without paying for a full Yocto build. control/ is copied
|
||||
# verbatim from libz1_1.3.2-r0_x86-64-v3.ipk built by poky-tiny (wrynose); the library itself is a stub
|
||||
# since only the archive shape and control metadata matter here.
|
||||
FROM alpine:3.18.4@sha256:48d9183eb12a05c99bcc0bf44a003607b8e941e1d4f41f9ad12bdcc4b5672f86 AS build
|
||||
|
||||
RUN apk add --no-cache binutils zstd
|
||||
|
||||
COPY control /pkg/control
|
||||
|
||||
RUN mkdir -p /pkg/data/usr/lib \
|
||||
&& echo stub > /pkg/data/usr/lib/libz.so.1.3.2 \
|
||||
&& ln -s libz.so.1.3.2 /pkg/data/usr/lib/libz.so.1 \
|
||||
&& cd /pkg \
|
||||
&& echo 2.0 > debian-binary \
|
||||
&& tar -C control -czf control.tar.gz . \
|
||||
&& tar -C data -cf - . | zstd -q -o data.tar.zst \
|
||||
&& ar rcD /libz1_1.3.2-r0_x86-64-v3.ipk debian-binary control.tar.gz data.tar.zst
|
||||
|
||||
FROM scratch
|
||||
COPY --from=build /libz1_1.3.2-r0_x86-64-v3.ipk /
|
||||
@@ -0,0 +1,15 @@
|
||||
Package: libz1
|
||||
Version: 1.3.2-r0
|
||||
Description: Zlib Compression Library
|
||||
Zlib is a general-purpose, patent-free, lossless data compression library
|
||||
which is used by many different programs.
|
||||
Section: libs
|
||||
Priority: optional
|
||||
Maintainer: Poky Maintainers <poky@lists.yoctoproject.org>
|
||||
License: Zlib
|
||||
Architecture: x86-64-v3
|
||||
OE: zlib
|
||||
Homepage: http://zlib.net/
|
||||
Depends: musl (>= 1.2.6+git0+9fa28ece75)
|
||||
Provides: zlib
|
||||
Source: zlib_1.3.2.bb
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
if [ x"$D" = "x" ]; then
|
||||
if [ -x /sbin/ldconfig ]; then /sbin/ldconfig ; fi
|
||||
fi
|
||||
@@ -12,18 +12,25 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
// maxBundleSearchSize bounds the bytes findSignatureOffset will hold at once while looking for the
|
||||
// bundle marker.
|
||||
// maxBundleSearchSize bounds how far into a file findSignatureOffset looks for the bundle marker.
|
||||
//
|
||||
// The marker sits inside the executable structure, so the search legitimately covers a whole
|
||||
// single-file bundle, which routinely runs past 100MB and can reach a few hundred for an app that
|
||||
// embeds sizable assets. Clamping to the file length alone is not enough: a mostly empty file costs
|
||||
// almost nothing inside a compressed layer, so a small artifact can still authorize a multi-gigabyte
|
||||
// allocation. The trade-off is that a bundle larger than this loses its deps.json rather than being
|
||||
// cataloged, which is the correct direction to fail when the alternative is OOM-killing the scan.
|
||||
// This mirrors maxDeclaredSectionSize in syft/internal/elfutil.
|
||||
// almost nothing inside a compressed layer, so a small artifact can still authorize an unbounded
|
||||
// amount of reading. The trade-off is that a bundle larger than this loses its deps.json rather than
|
||||
// being cataloged, which is the correct direction to fail. This mirrors maxDeclaredSectionSize in
|
||||
// syft/internal/elfutil. What the search holds in memory is bounded separately, by
|
||||
// markerSearchWindowSize, so raising this costs time rather than resident bytes.
|
||||
maxBundleSearchSize = 512 * intFile.MB
|
||||
|
||||
// markerSearchWindowSize is how much of a file findSignatureOffset holds at once.
|
||||
markerSearchWindowSize = 256 * intFile.KB
|
||||
|
||||
// bundleHeaderOffsetSize is the width of the little-endian header offset stored immediately before
|
||||
// the marker.
|
||||
bundleHeaderOffsetSize = 8
|
||||
|
||||
// maxDepsJSONSize bounds an embedded deps.json. These are dependency manifests, so real ones are
|
||||
// measured in KB even for large applications.
|
||||
maxDepsJSONSize = 3 * intFile.MB
|
||||
@@ -37,13 +44,26 @@ const (
|
||||
)
|
||||
|
||||
// dotNetBundleSignature is the SHA-256 hash of ".net core bundle" used to identify single-file bundles.
|
||||
var dotNetBundleSignature = []byte{
|
||||
var dotNetBundleSignature = [32]byte{
|
||||
0x8b, 0x12, 0x02, 0xb9, 0x6a, 0x61, 0x20, 0x38,
|
||||
0x72, 0x7b, 0x93, 0x02, 0x14, 0xd7, 0xa0, 0x32,
|
||||
0x13, 0xf5, 0xb9, 0xe6, 0xef, 0xae, 0x33, 0x18,
|
||||
0xee, 0x3b, 0x2d, 0xce, 0x24, 0xb3, 0x6a, 0xae,
|
||||
}
|
||||
|
||||
// markerSearchOverlap is how much of each search window carries over into the next: the bytes a marker
|
||||
// could straddle the boundary with, plus the offset that precedes it. Retaining both is what lets a
|
||||
// windowed search give the same answer as reading the file whole - a marker split across two reads is
|
||||
// still matched, and the offset in front of it is always in the same window as the match.
|
||||
const markerSearchOverlap = bundleHeaderOffsetSize + len(dotNetBundleSignature) - 1
|
||||
|
||||
// the slide at the end of findSignatureOffset carries markerSearchOverlap bytes into the next window and
|
||||
// advances by the rest, so a window no wider than the overlap would take a negative slice index and never
|
||||
// move. The window is five orders of magnitude clear of that today; this is what refuses to compile if
|
||||
// anyone shrinks it to the marker's own scale, which is the tempting thing to do to make a test cheaper.
|
||||
// The -1 is what makes equality fail too: the window has to exceed the overlap, not merely match it.
|
||||
const _ = uint(markerSearchWindowSize - markerSearchOverlap - 1)
|
||||
|
||||
// ExtractDepsJSON returns the deps.json embedded in the .NET single-file bundle in r, or "" if r carries no
|
||||
// bundle marker.
|
||||
//
|
||||
@@ -94,29 +114,17 @@ func findBundleHeaderOffset(r unionreader.UnionReader, searchLimit int64) (int64
|
||||
clamped = true
|
||||
}
|
||||
|
||||
// this scans a whole executable, routinely over 100MB for a single-file bundle, so the buffer is sized
|
||||
// exactly once. An append-growing read holds both arrays at its final growth and would cost well over
|
||||
// twice the file's own size for the same result.
|
||||
searchData := make([]byte, limit)
|
||||
|
||||
// a short read is not fatal here: the marker may well be in what we did get, so search the bytes we
|
||||
// actually hold. ReadAt reports a short read as io.EOF, and may report a full one that way too, so the
|
||||
// count is what says how much there is to search.
|
||||
n, err := r.ReadAt(searchData, 0)
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
headerOffset, found, err := findSignatureOffset(r, limit)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
idx := bytes.Index(searchData[:n], dotNetBundleSignature)
|
||||
if idx == -1 || idx < 8 {
|
||||
if !found {
|
||||
if clamped {
|
||||
return 0, fmt.Errorf("no bundle marker in the first %d bytes and the rest of the %d byte file was not searched", maxBundleSearchSize, size)
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
headerOffset := int64(binary.LittleEndian.Uint64(searchData[idx-8 : idx]))
|
||||
|
||||
if headerOffset == 0 {
|
||||
// the marker is compiled into every apphost; only publishing as a single file fills in the offset
|
||||
return 0, nil
|
||||
@@ -131,6 +139,52 @@ func findBundleHeaderOffset(r unionreader.UnionReader, searchLimit int64) (int64
|
||||
return headerOffset, nil
|
||||
}
|
||||
|
||||
// findSignatureOffset searches the first limit bytes of r for the bundle marker and returns the header
|
||||
// offset stored in the 8 bytes immediately before it, or false if the marker is not there.
|
||||
func findSignatureOffset(r unionreader.UnionReader, limit int64) (int64, bool, error) {
|
||||
// nothing shorter than the marker plus the offset in front of it can hold a usable match
|
||||
if limit < int64(bundleHeaderOffsetSize+len(dotNetBundleSignature)) {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
buf := make([]byte, min(int64(markerSearchWindowSize), limit))
|
||||
|
||||
var (
|
||||
windowStart int64 // offset in the file that buf[0] holds
|
||||
filled int // bytes of buf that hold file content
|
||||
)
|
||||
|
||||
for {
|
||||
// a short read is not fatal: the marker may well be in what we did get. ReadAt reports a short
|
||||
// read as io.EOF, and may report a full one that way too, so the count is what says how much
|
||||
// there is to search.
|
||||
want := min(int64(len(buf)-filled), limit-windowStart-int64(filled))
|
||||
n, err := r.ReadAt(buf[filled:int64(filled)+want], windowStart+int64(filled))
|
||||
filled += n
|
||||
if err != nil && !errors.Is(err, io.EOF) {
|
||||
return 0, false, err
|
||||
}
|
||||
|
||||
if idx := bytes.Index(buf[:filled], dotNetBundleSignature[:]); idx >= 0 {
|
||||
// only reachable in the first window: after a slide the overlap guarantees that a match not
|
||||
// already seen begins at least bundleHeaderOffsetSize into the buffer
|
||||
if idx < bundleHeaderOffsetSize {
|
||||
return 0, false, nil
|
||||
}
|
||||
return int64(binary.LittleEndian.Uint64(buf[idx-bundleHeaderOffsetSize : idx])), true, nil
|
||||
}
|
||||
|
||||
// a buffer the read could not fill means the reader ran out before the limit did
|
||||
if filled < len(buf) || windowStart+int64(filled) >= limit {
|
||||
return 0, false, nil
|
||||
}
|
||||
|
||||
copy(buf, buf[filled-markerSearchOverlap:filled])
|
||||
windowStart += int64(filled - markerSearchOverlap)
|
||||
filled = markerSearchOverlap
|
||||
}
|
||||
}
|
||||
|
||||
// dotNetBundleHeader represents the fixed portion of the bundle header (version 1+)
|
||||
type dotNetBundleHeader struct {
|
||||
MajorVersion uint32
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
func fileWithSignatureAt(size, sigStart int, headerOffset uint64) []byte {
|
||||
data := make([]byte, size)
|
||||
binary.LittleEndian.PutUint64(data[sigStart-8:sigStart], headerOffset)
|
||||
copy(data[sigStart:], dotNetBundleSignature)
|
||||
copy(data[sigStart:], dotNetBundleSignature[:])
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ func TestFindBundleHeaderOffset(t *testing.T) {
|
||||
{
|
||||
// there is no room for the 8-byte header offset before the signature
|
||||
name: "signature too close to the start to carry an offset",
|
||||
data: append(append([]byte{0, 0}, dotNetBundleSignature...), make([]byte, 32)...),
|
||||
data: append(append([]byte{0, 0}, dotNetBundleSignature[:]...), make([]byte, 32)...),
|
||||
searchLimit: 128,
|
||||
},
|
||||
{
|
||||
@@ -295,3 +295,136 @@ func TestFindDepsJSONInManifest_PlausibleEntryCountIsWalked(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, got, "no deps.json entry in this manifest")
|
||||
}
|
||||
|
||||
// A windowed search only gives the same answer as reading the file whole if a marker lying across a
|
||||
// window boundary - or one whose header offset lies across it - is still matched. These are the offsets
|
||||
// where an overlap that is absent, too small, or applied to the wrong end of the buffer goes wrong.
|
||||
func TestFindBundleHeaderOffset_MarkerAcrossWindowBoundary(t *testing.T) {
|
||||
const window = markerSearchWindowSize
|
||||
const headerOffset = 0x1234
|
||||
size := window + 4096
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
sigStart int
|
||||
}{
|
||||
{"whole marker inside the first window", window - 4096},
|
||||
{"header offset split across the boundary", window - 4},
|
||||
{"header offset ends exactly at the boundary", window},
|
||||
{"signature split one byte into the second window", window - len(dotNetBundleSignature) + 1},
|
||||
{"signature split down the middle", window - len(dotNetBundleSignature)/2},
|
||||
{"signature starts one byte before the boundary", window - 1},
|
||||
{"whole marker inside the second window", window + 2048},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
data := fileWithSignatureAt(size, tt.sigStart, headerOffset)
|
||||
|
||||
got, err := findBundleHeaderOffset(readSeekCloser{bytes.NewReader(data)}, int64(size))
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, int64(headerOffset), got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// recordingReader records the largest single read it is asked for. The search buffer is what bounds
|
||||
// that length, so this is what fails if the implementation ever sizes a buffer against the file again.
|
||||
type recordingReader struct {
|
||||
*bytes.Reader
|
||||
largestRead int
|
||||
}
|
||||
|
||||
func (r *recordingReader) ReadAt(p []byte, off int64) (int, error) {
|
||||
if len(p) > r.largestRead {
|
||||
r.largestRead = len(p)
|
||||
}
|
||||
return r.Reader.ReadAt(p, off)
|
||||
}
|
||||
|
||||
func (*recordingReader) Close() error { return nil }
|
||||
|
||||
// Holding an executable at its full size to look for 32 bytes is what made this the largest single
|
||||
// consumer of memory in a scan of a Windows volume, so the bound matters more than the result here.
|
||||
func TestFindBundleHeaderOffset_ReadsAreBoundedByTheWindow(t *testing.T) {
|
||||
size := 5 * markerSearchWindowSize
|
||||
data := fileWithSignatureAt(size, size-1024, 0x1234)
|
||||
r := &recordingReader{Reader: bytes.NewReader(data)}
|
||||
|
||||
got, err := findBundleHeaderOffset(r, int64(size))
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, int64(0x1234), got, "the marker must still be found at the far end of the file")
|
||||
assert.LessOrEqual(t, r.largestRead, markerSearchWindowSize,
|
||||
"no single read may exceed the search window, whatever the file's size")
|
||||
}
|
||||
|
||||
// Every window after the first begins with bytes already searched. Re-reporting a match from that
|
||||
// overlap would be harmless, but failing to advance past it would spin forever.
|
||||
func TestFindBundleHeaderOffset_UnmarkedFileTerminates(t *testing.T) {
|
||||
for _, size := range []int{
|
||||
markerSearchWindowSize - 1,
|
||||
markerSearchWindowSize,
|
||||
markerSearchWindowSize + 1,
|
||||
markerSearchWindowSize + markerSearchOverlap,
|
||||
3 * markerSearchWindowSize,
|
||||
} {
|
||||
got, err := findBundleHeaderOffset(readSeekCloser{bytes.NewReader(make([]byte, size))}, int64(size))
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Zero(t, got)
|
||||
}
|
||||
}
|
||||
|
||||
// lateShortReader reads cleanly until shortFrom and only then hands back fewer bytes than were asked for.
|
||||
// shortReader above comes up short on its very first read, which the search answers before it has slid at
|
||||
// all; the reader running out partway through a windowed search is a different branch.
|
||||
type lateShortReader struct {
|
||||
*bytes.Reader
|
||||
shortFrom int64 // a read starting here or beyond stops after shortLen bytes
|
||||
shortLen int
|
||||
}
|
||||
|
||||
func (r *lateShortReader) ReadAt(p []byte, off int64) (int, error) {
|
||||
if off < r.shortFrom || len(p) <= r.shortLen {
|
||||
return r.Reader.ReadAt(p, off)
|
||||
}
|
||||
|
||||
n, _ := r.Reader.ReadAt(p[:r.shortLen], off)
|
||||
return n, io.EOF
|
||||
}
|
||||
|
||||
func (*lateShortReader) Close() error { return nil }
|
||||
|
||||
// A squashfs block that decompresses short makes unionreader return fewer bytes than were asked for, and
|
||||
// nothing says that lands in the first window. Whatever the search did get still has to be searched, and the
|
||||
// bytes the reader never produced have to end it rather than being read as a file with no bundle in it.
|
||||
func TestFindBundleHeaderOffset_ShortReadAfterASlideSearchesWhatWasRead(t *testing.T) {
|
||||
const window = markerSearchWindowSize
|
||||
const shortLen = 4096 // what the second window gets before the reader gives up
|
||||
const headerOffset = 0x1234
|
||||
size := 2*window + 8192
|
||||
|
||||
for _, tt := range []struct {
|
||||
name string
|
||||
sigStart int
|
||||
want int64
|
||||
}{
|
||||
{"marker inside the bytes the second window did get", window + 1024, headerOffset},
|
||||
{"marker straddling the boundary into those bytes", window - 4, headerOffset},
|
||||
{"marker beyond where the reader gave up", window + shortLen + 1024, 0},
|
||||
} {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// honest about its length: only the read past the first window comes up short
|
||||
r := &lateShortReader{
|
||||
Reader: bytes.NewReader(fileWithSignatureAt(size, tt.sigStart, headerOffset)),
|
||||
shortFrom: window,
|
||||
shortLen: shortLen,
|
||||
}
|
||||
|
||||
got, err := findBundleHeaderOffset(r, int64(size))
|
||||
|
||||
require.NoError(t, err, "a reader that runs out is not a parse failure")
|
||||
assert.Equal(t, tt.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user