Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
114ba0a29f |
@@ -16,6 +16,7 @@ linters:
|
|||||||
- depguard # Dependency allow/block lists
|
- depguard # Dependency allow/block lists
|
||||||
- godot # Requires comments to end with periods
|
- godot # Requires comments to end with periods
|
||||||
- wsl # Deprecated, replaced by wsl_v5
|
- wsl # Deprecated, replaced by wsl_v5
|
||||||
|
- gomodguard # Deprecated, replaced by gomodguard_v2
|
||||||
- wrapcheck # Too verbose for internal packages
|
- wrapcheck # Too verbose for internal packages
|
||||||
- varnamelen # Short names like db, id are idiomatic Go
|
- varnamelen # Short names like db, id are idiomatic Go
|
||||||
settings:
|
settings:
|
||||||
|
|||||||
+6
-2
@@ -8,6 +8,9 @@ RUN go mod download
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Touch .pb.go so make does not try to regenerate via protoc (file is committed)
|
||||||
|
RUN touch mfer/mf.pb.go
|
||||||
|
|
||||||
# Go half of fmt-check only: this image has no node, so no prettier. The
|
# Go half of fmt-check only: this image has no node, so no prettier. The
|
||||||
# markdown half runs in the mdfmt stage below.
|
# markdown half runs in the mdfmt stage below.
|
||||||
RUN make fmt-check-go
|
RUN make fmt-check-go
|
||||||
@@ -43,6 +46,9 @@ RUN go mod download
|
|||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Touch .pb.go so make does not try to regenerate via protoc (file is committed)
|
||||||
|
RUN touch mfer/mf.pb.go
|
||||||
|
|
||||||
RUN make test
|
RUN make test
|
||||||
|
|
||||||
# A build context sent as a tar archive, as upaas sends it, keeps its files'
|
# A build context sent as a tar archive, as upaas sends it, keeps its files'
|
||||||
@@ -69,7 +75,5 @@ RUN version="${VERSION:-$(git describe --tags --always)}"; \
|
|||||||
RUN ldd /mfer 2>&1 | grep -q 'not a dynamic executable'
|
RUN ldd /mfer 2>&1 | grep -q 'not a dynamic executable'
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
# scratch has no CA certificates; fetch needs them to verify HTTPS servers.
|
|
||||||
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
|
|
||||||
COPY --from=builder /mfer /mfer
|
COPY --from=builder /mfer /mfer
|
||||||
ENTRYPOINT ["/mfer"]
|
ENTRYPOINT ["/mfer"]
|
||||||
|
|||||||
@@ -49,9 +49,7 @@ The `innerMessage` field is compressed with
|
|||||||
enforce a decompression size limit to prevent decompression bombs. The reference
|
enforce a decompression size limit to prevent decompression bombs. The reference
|
||||||
implementation limits decompressed size to 256 MB. It writes zstd frames with a
|
implementation limits decompressed size to 256 MB. It writes zstd frames with a
|
||||||
window of at most 8 MiB, the largest window the zstd format recommends decoders
|
window of at most 8 MiB, the largest window the zstd format recommends decoders
|
||||||
support, and refuses frames that ask for a larger one. It also refuses an inner
|
support, and refuses frames that ask for a larger one.
|
||||||
message whose file entries, hashes, timestamps and MIME types, counted at 160,
|
|
||||||
112, 64 and 16 bytes each, add up to more than 8 times its size.
|
|
||||||
|
|
||||||
## Inner Message (`MFFile`)
|
## Inner Message (`MFFile`)
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export DOCKER_BUILDKIT := 1
|
|||||||
export PROGRESS_NO_TRUNC := 1
|
export PROGRESS_NO_TRUNC := 1
|
||||||
GOPATH := $(shell go env GOPATH)
|
GOPATH := $(shell go env GOPATH)
|
||||||
export PATH := $(PATH):$(GOPATH)/bin
|
export PATH := $(PATH):$(GOPATH)/bin
|
||||||
|
PROTOC_GEN_GO := $(GOPATH)/bin/protoc-gen-go
|
||||||
SOURCEFILES := mfer/*.go mfer/*.proto internal/*/*.go cmd/*/*.go go.mod go.sum
|
SOURCEFILES := mfer/*.go mfer/*.proto internal/*/*.go cmd/*/*.go go.mod go.sum
|
||||||
ARCH := $(shell uname -m)
|
ARCH := $(shell uname -m)
|
||||||
GITREV_BUILD := $(shell bash $(PWD)/bin/gitrev.sh 2>/dev/null || echo unknown)
|
GITREV_BUILD := $(shell bash $(PWD)/bin/gitrev.sh 2>/dev/null || echo unknown)
|
||||||
@@ -12,7 +13,7 @@ GOLDFLAGS += -X main.Version=$(VERSION)
|
|||||||
GOLDFLAGS += -X main.Gitrev=$(GITREV_BUILD)
|
GOLDFLAGS += -X main.Gitrev=$(GITREV_BUILD)
|
||||||
GOFLAGS := -ldflags "$(GOLDFLAGS)"
|
GOFLAGS := -ldflags "$(GOLDFLAGS)"
|
||||||
|
|
||||||
.PHONY: bootstrap setup docker default run ci test fuzz check lint fmt fmt-check fmt-check-go fmt-check-md hooks fixme generate
|
.PHONY: bootstrap setup docker default run ci test fuzz check lint fmt fmt-check fmt-check-go fmt-check-md hooks fixme
|
||||||
|
|
||||||
default: fmt test
|
default: fmt test
|
||||||
|
|
||||||
@@ -34,6 +35,9 @@ test:
|
|||||||
fuzz:
|
fuzz:
|
||||||
@script/fuzz
|
@script/fuzz
|
||||||
|
|
||||||
|
$(PROTOC_GEN_GO):
|
||||||
|
test -e $(PROTOC_GEN_GO) || go install -v google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1
|
||||||
|
|
||||||
fixme:
|
fixme:
|
||||||
@grep -nir fixme . | grep -v Makefile
|
@grep -nir fixme . | grep -v Makefile
|
||||||
|
|
||||||
@@ -54,14 +58,15 @@ fmt-check-md:
|
|||||||
hooks:
|
hooks:
|
||||||
@script/install-precommit
|
@script/install-precommit
|
||||||
|
|
||||||
generate:
|
mfer/mf.pb.go: mfer/mf.proto
|
||||||
@script/generate
|
cd mfer && go generate .
|
||||||
|
|
||||||
bin/mfer: $(SOURCEFILES)
|
bin/mfer: $(SOURCEFILES) mfer/mf.pb.go
|
||||||
|
protoc --version
|
||||||
cd cmd/mfer && go build -tags urfave_cli_no_docs -o ../../bin/mfer $(GOFLAGS) .
|
cd cmd/mfer && go build -tags urfave_cli_no_docs -o ../../bin/mfer $(GOFLAGS) .
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rfv bin/mfer cmd/mfer/mfer *.dockerimage
|
rm -rfv mfer/*.pb.go bin/mfer cmd/mfer/mfer *.dockerimage
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
@script/fmt
|
@script/fmt
|
||||||
|
|||||||
@@ -72,19 +72,8 @@ provide:
|
|||||||
`script/bootstrap`, then `script/install-precommit`
|
`script/bootstrap`, then `script/install-precommit`
|
||||||
- `script/projectname` — output the project name (`mfer`); used by other scripts
|
- `script/projectname` — output the project name (`mfer`); used by other scripts
|
||||||
such as `script/docker`
|
such as `script/docker`
|
||||||
- `script/test` — run the test suite (`go test`); one test fails when
|
- `script/test` — run the test suite (`go test`), regenerating the protobuf code
|
||||||
`mfer/mf.proto` no longer matches the hash `script/generate` recorded
|
first if it is stale
|
||||||
- `script/generate` (`make generate`) — regenerate `mfer/mf.pb.go` from
|
|
||||||
`mfer/mf.proto` and record the hash of that `mfer/mf.proto` in
|
|
||||||
`mfer/mf.proto.sha256`; the only thing that regenerates the committed
|
|
||||||
`mfer/mf.pb.go`. It needs the exact versions that wrote the committed file,
|
|
||||||
and refuses to run with any other: `protoc` 33.4 (unpack
|
|
||||||
`protoc-33.4-<platform>.zip` from
|
|
||||||
[its release](https://github.com/protocolbuffers/protobuf/releases/tag/v33.4)
|
|
||||||
and put its `bin/protoc` on `PATH`) and `protoc-gen-go` v1.36.11
|
|
||||||
(`go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.36.11`, which
|
|
||||||
installs it in `$(go env GOPATH)/bin`; `make generate` adds that directory to
|
|
||||||
`PATH`)
|
|
||||||
- `script/fuzz` — fuzz the manifest parser for one minute; run by hand
|
- `script/fuzz` — fuzz the manifest parser for one minute; run by hand
|
||||||
(`make fuzz`), never by CI, while `script/test` runs its committed seed corpus
|
(`make fuzz`), never by CI, while `script/test` runs its committed seed corpus
|
||||||
as ordinary tests
|
as ordinary tests
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ require (
|
|||||||
github.com/pterm/pterm v0.12.35
|
github.com/pterm/pterm v0.12.35
|
||||||
github.com/spf13/afero v1.8.0
|
github.com/spf13/afero v1.8.0
|
||||||
github.com/stretchr/testify v1.8.1
|
github.com/stretchr/testify v1.8.1
|
||||||
github.com/urfave/cli/v2 v2.27.7
|
github.com/urfave/cli/v2 v2.23.6
|
||||||
google.golang.org/protobuf v1.28.1
|
google.golang.org/protobuf v1.28.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/atomicgo/cursor v0.0.1 // indirect
|
github.com/atomicgo/cursor v0.0.1 // indirect
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
github.com/fatih/color v1.7.0 // indirect
|
github.com/fatih/color v1.7.0 // indirect
|
||||||
github.com/gookit/color v1.4.2 // indirect
|
github.com/gookit/color v1.4.2 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
github.com/klauspost/cpuid/v2 v2.0.9 // indirect
|
||||||
@@ -34,7 +34,7 @@ require (
|
|||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
|
||||||
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
|
golang.org/x/crypto v0.0.0-20220525230936-793ad666bf5e // indirect
|
||||||
golang.org/x/sys v0.1.0 // indirect
|
golang.org/x/sys v0.1.0 // indirect
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
|
|||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||||
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
@@ -231,12 +231,12 @@ github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj
|
|||||||
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
|
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
|
||||||
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
|
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
|
||||||
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
|
github.com/tj/go-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
|
||||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
github.com/urfave/cli/v2 v2.23.6 h1:iWmtKD+prGo1nKUtLO0Wg4z9esfBM4rAV4QRLQiEmJ4=
|
||||||
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
github.com/urfave/cli/v2 v2.23.6/go.mod h1:GHupkWPMM0M/sj1a2b4wUrWBPzazNrIjouW6fmdJLxc=
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8=
|
||||||
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8=
|
||||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
|||||||
@@ -298,11 +298,7 @@ func (mfa *CLIApp) checkManifestOperation(ctx *cli.Context) error {
|
|||||||
log.Infof("checking manifest %s with base %s", manifestPath, basePath)
|
log.Infof("checking manifest %s with base %s", manifestPath, basePath)
|
||||||
|
|
||||||
// Create checker
|
// Create checker
|
||||||
chk, err := mfer.NewChecker(&mfer.CheckerOptions{
|
chk, err := mfer.NewChecker(manifestPath, basePath, mfa.Fs)
|
||||||
ManifestPath: manifestPath,
|
|
||||||
BasePath: basePath,
|
|
||||||
Fs: mfa.Fs,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to load manifest: %w", err)
|
return fmt.Errorf("failed to load manifest: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-112
@@ -8,8 +8,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -32,7 +30,6 @@ const (
|
|||||||
testFlagBase = "--base"
|
testFlagBase = "--base"
|
||||||
testFlagNoExtra = "--no-extra-files"
|
testFlagNoExtra = "--no-extra-files"
|
||||||
testFlagVersion = "--version"
|
testFlagVersion = "--version"
|
||||||
testFlagVerbose = "--verbose"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var errSimulatedWrite = errors.New("simulated write failure")
|
var errSimulatedWrite = errors.New("simulated write failure")
|
||||||
@@ -45,32 +42,20 @@ var errSimulatedWrite = errors.New("simulated write failure")
|
|||||||
var runMu sync.Mutex
|
var runMu sync.Mutex
|
||||||
|
|
||||||
// runCLI invokes RunWithOptions while holding runMu so parallel tests
|
// runCLI invokes RunWithOptions while holding runMu so parallel tests
|
||||||
// capture their own output, and returns its exit code.
|
// capture their own output. Before releasing the lock it points the
|
||||||
|
// process-global logger at io.Discard: other tests log outside the lock
|
||||||
|
// (manifest loads, scans), and those lines must not land in this run's
|
||||||
|
// buffers once it has returned and its test is reading them.
|
||||||
func runCLI(opts *RunOptions) int {
|
func runCLI(opts *RunOptions) int {
|
||||||
exitCode, _ := runCLIWithLevel(opts)
|
|
||||||
|
|
||||||
return exitCode
|
|
||||||
}
|
|
||||||
|
|
||||||
// runCLIWithLevel is runCLI that also returns the log level the run left
|
|
||||||
// set, read while runMu still keeps other runs from changing it. Each run
|
|
||||||
// starts at the default level, as a new process does. Before releasing the
|
|
||||||
// lock it points the process-global logger at io.Discard: other tests log
|
|
||||||
// outside the lock (manifest loads, scans), and those lines must not land in
|
|
||||||
// this run's buffers once it has returned and its test is reading them.
|
|
||||||
func runCLIWithLevel(opts *RunOptions) (int, log.Level) {
|
|
||||||
runMu.Lock()
|
runMu.Lock()
|
||||||
defer runMu.Unlock()
|
defer runMu.Unlock()
|
||||||
|
|
||||||
log.SetLevel(log.InfoLevel)
|
|
||||||
|
|
||||||
exitCode := RunWithOptions(opts)
|
exitCode := RunWithOptions(opts)
|
||||||
level := log.GetLevel()
|
|
||||||
|
|
||||||
log.SetOutput(io.Discard, io.Discard)
|
log.SetOutput(io.Discard, io.Discard)
|
||||||
log.Init()
|
log.Init()
|
||||||
|
|
||||||
return exitCode, level
|
return exitCode
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMain(m *testing.M) {
|
func TestMain(m *testing.M) {
|
||||||
@@ -250,90 +235,6 @@ func TestRootVerbosityFlags(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// commandsTakingVerbose returns the command lines -v can follow: the root and
|
|
||||||
// the generate, check, freshen and fetch subcommands.
|
|
||||||
func commandsTakingVerbose() [][]string {
|
|
||||||
return [][]string{
|
|
||||||
{testApp},
|
|
||||||
{testApp, cmdGenerate},
|
|
||||||
{testApp, cmdCheck},
|
|
||||||
{testApp, cmdFreshen},
|
|
||||||
{testApp, cmdFetch},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestVerboseCount asserts that one -v or --verbose gives verbose output and
|
|
||||||
// two -v give debug output (issue #125). urfave/cli before v2.25.5 counted a
|
|
||||||
// flag given by its alias twice, so one -v gave debug output.
|
|
||||||
func TestVerboseCount(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
cases := []struct {
|
|
||||||
flags []string
|
|
||||||
want log.Level
|
|
||||||
}{
|
|
||||||
{[]string{"-v"}, log.VerboseLevel},
|
|
||||||
{[]string{testFlagVerbose}, log.VerboseLevel},
|
|
||||||
{[]string{"-v", "-v"}, log.DebugLevel},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, command := range commandsTakingVerbose() {
|
|
||||||
for _, tc := range cases {
|
|
||||||
args := slices.Concat(command, tc.flags)
|
|
||||||
|
|
||||||
t.Run(strings.Join(args, " "), func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
_, level := runCLIWithLevel(testOpts(args, afero.NewMemMapFs()))
|
|
||||||
assert.Equal(t, tc.want, level)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestCombinedShortVerboseRefused asserts that -vv is refused (issue #125):
|
|
||||||
// single-letter flags do not combine, so the -v help text says -v -v.
|
|
||||||
func TestCombinedShortVerboseRefused(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, command := range commandsTakingVerbose() {
|
|
||||||
args := slices.Concat(command, []string{"-vv"})
|
|
||||||
|
|
||||||
t.Run(strings.Join(args, " "), func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
opts := testOpts(args, afero.NewMemMapFs())
|
|
||||||
exitCode, level := runCLIWithLevel(opts)
|
|
||||||
|
|
||||||
assert.Equal(t, 1, exitCode)
|
|
||||||
assert.Contains(t, testStderr(t, opts), "flag provided but not defined: -vv")
|
|
||||||
assert.Equal(t, log.InfoLevel, level)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestShortAndLongVerboseRefused asserts that -v and --verbose given together
|
|
||||||
// are refused (issue #125): urfave/cli v2 refuses a flag given under two of its
|
|
||||||
// names, and one flag with an alias keeps help and parsing simple.
|
|
||||||
func TestShortAndLongVerboseRefused(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, command := range commandsTakingVerbose() {
|
|
||||||
args := slices.Concat(command, []string{"-v", testFlagVerbose})
|
|
||||||
|
|
||||||
t.Run(strings.Join(args, " "), func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
opts := testOpts(args, afero.NewMemMapFs())
|
|
||||||
exitCode, level := runCLIWithLevel(opts)
|
|
||||||
|
|
||||||
assert.Equal(t, 1, exitCode)
|
|
||||||
assert.Contains(t, testStderr(t, opts), "Cannot use two forms of the same flag")
|
|
||||||
assert.Equal(t, log.InfoLevel, level)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestHelpCommand(t *testing.T) {
|
func TestHelpCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -504,10 +405,7 @@ func TestGenerateExcludesDotfilesByDefault(t *testing.T) {
|
|||||||
assert.True(t, exists)
|
assert.True(t, exists)
|
||||||
|
|
||||||
// Verify manifest only has 1 file (the non-dotfile)
|
// Verify manifest only has 1 file (the non-dotfile)
|
||||||
manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{
|
manifest, err := mfer.NewManifestFromFile(fs, testMF)
|
||||||
Path: testMF,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, manifest.Files(), 1)
|
assert.Len(t, manifest.Files(), 1)
|
||||||
assert.Equal(t, "file1.txt", manifest.Files()[0].GetPath())
|
assert.Equal(t, "file1.txt", manifest.Files()[0].GetPath())
|
||||||
@@ -531,10 +429,7 @@ func TestGenerateWithIncludeDotfiles(t *testing.T) {
|
|||||||
require.Equal(t, 0, exitCode)
|
require.Equal(t, 0, exitCode)
|
||||||
|
|
||||||
// Verify manifest has 2 files (including dotfile)
|
// Verify manifest has 2 files (including dotfile)
|
||||||
manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{
|
manifest, err := mfer.NewManifestFromFile(fs, testMF)
|
||||||
Path: testMF,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, manifest.Files(), 2)
|
assert.Len(t, manifest.Files(), 2)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,11 +61,7 @@ func unsignedChecker(t *testing.T) *mfer.Checker {
|
|||||||
require.NoError(t, s.ToManifest(context.Background(), &buf, nil))
|
require.NoError(t, s.ToManifest(context.Background(), &buf, nil))
|
||||||
require.NoError(t, afero.WriteFile(fs, "/d/index.mf", buf.Bytes(), 0o644))
|
require.NoError(t, afero.WriteFile(fs, "/d/index.mf", buf.Bytes(), 0o644))
|
||||||
|
|
||||||
chk, err := mfer.NewChecker(&mfer.CheckerOptions{
|
chk, err := mfer.NewChecker("/d/index.mf", "/d", fs)
|
||||||
ManifestPath: "/d/index.mf",
|
|
||||||
BasePath: "/d",
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.False(t, chk.IsSigned())
|
require.False(t, chk.IsSigned())
|
||||||
|
|
||||||
@@ -171,11 +167,7 @@ func signedChecker(t *testing.T) *mfer.Checker {
|
|||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
require.NoError(t, afero.WriteFile(fs, "/index.mf", buf.Bytes(), 0o644))
|
require.NoError(t, afero.WriteFile(fs, "/index.mf", buf.Bytes(), 0o644))
|
||||||
|
|
||||||
chk, err := mfer.NewChecker(&mfer.CheckerOptions{
|
chk, err := mfer.NewChecker("/index.mf", "/", fs)
|
||||||
ManifestPath: "/index.mf",
|
|
||||||
BasePath: "/",
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.True(t, chk.IsSigned())
|
require.True(t, chk.IsSigned())
|
||||||
|
|
||||||
|
|||||||
+2
-13
@@ -445,10 +445,7 @@ func (mfa *CLIApp) loadExistingEntries(
|
|||||||
log.Infof("loading manifest from %s", manifestPath)
|
log.Infof("loading manifest from %s", manifestPath)
|
||||||
|
|
||||||
// Load existing manifest
|
// Load existing manifest
|
||||||
manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{
|
manifest, err := mfer.NewManifestFromFile(mfa.Fs, manifestPath)
|
||||||
Path: manifestPath,
|
|
||||||
Fs: mfa.Fs,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load manifest: %w", err)
|
return nil, fmt.Errorf("failed to load manifest: %w", err)
|
||||||
}
|
}
|
||||||
@@ -615,15 +612,7 @@ func addExistingToBuilder(b *mfer.Builder, entry *mfer.MFFilePath) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
err := b.AddFileWithHash(mfer.RelFilePath(entry.GetPath()),
|
return b.AddFileWithHash(mfer.RelFilePath(entry.GetPath()),
|
||||||
mfer.FileSize(entry.GetSize()), mfer.ModTime(mtime),
|
mfer.FileSize(entry.GetSize()), mfer.ModTime(mtime),
|
||||||
entry.GetHashes()[0].GetMultiHash())
|
entry.GetHashes()[0].GetMultiHash())
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"manifest entry %s: %w (regenerate the manifest with mfer generate)",
|
|
||||||
entry.GetPath(), err,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,12 +4,10 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
|
||||||
"os"
|
"os"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/multiformats/go-multihash"
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -60,10 +58,7 @@ func TestFreshenUnchanged(t *testing.T) {
|
|||||||
setupFreshenDir(t, fs)
|
setupFreshenDir(t, fs)
|
||||||
|
|
||||||
// Parse manifest to verify
|
// Parse manifest to verify
|
||||||
manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{
|
manifest, err := mfer.NewManifestFromFile(fs, "/testdir/.index.mf")
|
||||||
Path: "/testdir/.index.mf",
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, manifest.Files(), 2)
|
assert.Len(t, manifest.Files(), 2)
|
||||||
}
|
}
|
||||||
@@ -75,10 +70,7 @@ func TestFreshenWithChanges(t *testing.T) {
|
|||||||
setupFreshenDir(t, fs)
|
setupFreshenDir(t, fs)
|
||||||
|
|
||||||
// Verify initial manifest has 2 files
|
// Verify initial manifest has 2 files
|
||||||
manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{
|
manifest, err := mfer.NewManifestFromFile(fs, "/testdir/.index.mf")
|
||||||
Path: "/testdir/.index.mf",
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Len(t, manifest.Files(), 2)
|
assert.Len(t, manifest.Files(), 2)
|
||||||
|
|
||||||
@@ -177,50 +169,21 @@ func TestFreshenRecordEntryMtimePresence(t *testing.T) {
|
|||||||
func TestFreshenAddExistingRejectsMissingMtime(t *testing.T) {
|
func TestFreshenAddExistingRejectsMissingMtime(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
hash, err := multihash.Encode(make([]byte, sha256.Size), multihash.SHA2_256)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
b := mfer.NewBuilder()
|
b := mfer.NewBuilder()
|
||||||
entry := &mfer.MFFilePath{
|
entry := &mfer.MFFilePath{
|
||||||
Path: "file1.txt",
|
Path: "file1.txt",
|
||||||
Size: 8,
|
Size: 8,
|
||||||
Mtime: nil,
|
Mtime: nil,
|
||||||
Hashes: []*mfer.MFFileChecksum{
|
Hashes: []*mfer.MFFileChecksum{
|
||||||
{MultiHash: hash},
|
{MultiHash: []byte{0x12, 0x20}},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
err = addExistingToBuilder(b, entry)
|
err := addExistingToBuilder(b, entry)
|
||||||
require.ErrorIs(t, err, errEntryMissingMtime)
|
require.ErrorIs(t, err, errEntryMissingMtime)
|
||||||
assert.Contains(t, err.Error(), "file1.txt")
|
assert.Contains(t, err.Error(), "file1.txt")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestFreshenAddExistingRejectsShortHash pins that an existing manifest
|
|
||||||
// entry whose hash the builder refuses is reported as a problem with the
|
|
||||||
// manifest, with the command that fixes it.
|
|
||||||
func TestFreshenAddExistingRejectsShortHash(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
sha1Hash, err := multihash.Encode(make([]byte, 20), multihash.SHA1)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
b := mfer.NewBuilder()
|
|
||||||
entry := &mfer.MFFilePath{
|
|
||||||
Path: "old.txt",
|
|
||||||
Size: 8,
|
|
||||||
Mtime: &mfer.Timestamp{Seconds: 1_700_000_000},
|
|
||||||
Hashes: []*mfer.MFFileChecksum{
|
|
||||||
{MultiHash: sha1Hash},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err = addExistingToBuilder(b, entry)
|
|
||||||
require.Error(t, err)
|
|
||||||
assert.Contains(t, err.Error(), "manifest entry old.txt")
|
|
||||||
assert.Contains(t, err.Error(), "mfer generate")
|
|
||||||
assert.Zero(t, b.FileCount())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestEntryMtime pins the presence semantics the callers depend on.
|
// TestEntryMtime pins the presence semantics the callers depend on.
|
||||||
func TestEntryMtime(t *testing.T) {
|
func TestEntryMtime(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|||||||
+7
-10
@@ -17,7 +17,6 @@ import (
|
|||||||
const (
|
const (
|
||||||
cmdGenerate = "generate"
|
cmdGenerate = "generate"
|
||||||
cmdCheck = "check"
|
cmdCheck = "check"
|
||||||
cmdFreshen = "freshen"
|
|
||||||
cmdExport = "export"
|
cmdExport = "export"
|
||||||
cmdFetch = "fetch"
|
cmdFetch = "fetch"
|
||||||
cmdVersion = "version"
|
cmdVersion = "version"
|
||||||
@@ -119,7 +118,7 @@ func commonFlags() []cli.Flag {
|
|||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "verbose",
|
Name: "verbose",
|
||||||
Aliases: []string{"v"},
|
Aliases: []string{"v"},
|
||||||
Usage: "Increase verbosity (-v for verbose, -v -v for debug)",
|
Usage: "Increase verbosity (-v for verbose, -vv for debug)",
|
||||||
Count: new(int),
|
Count: new(int),
|
||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
@@ -132,10 +131,9 @@ func commonFlags() []cli.Flag {
|
|||||||
|
|
||||||
func (mfa *CLIApp) generateCommand() *cli.Command {
|
func (mfa *CLIApp) generateCommand() *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: cmdGenerate,
|
Name: cmdGenerate,
|
||||||
Aliases: []string{"gen"},
|
Aliases: []string{"gen"},
|
||||||
Usage: "Generate manifest file",
|
Usage: "Generate manifest file",
|
||||||
ArgsUsage: "[path ...]",
|
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
mfa.setVerbosity(c)
|
mfa.setVerbosity(c)
|
||||||
mfa.printBanner()
|
mfa.printBanner()
|
||||||
@@ -229,7 +227,7 @@ func (mfa *CLIApp) checkCommand() *cli.Command {
|
|||||||
|
|
||||||
func (mfa *CLIApp) freshenCommand() *cli.Command {
|
func (mfa *CLIApp) freshenCommand() *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: cmdFreshen,
|
Name: "freshen",
|
||||||
Usage: "Update manifest with changed, new, and removed files",
|
Usage: "Update manifest with changed, new, and removed files",
|
||||||
ArgsUsage: manifestArgsUsage,
|
ArgsUsage: manifestArgsUsage,
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
@@ -326,9 +324,8 @@ func (mfa *CLIApp) listCommand() *cli.Command {
|
|||||||
|
|
||||||
func (mfa *CLIApp) fetchCommand() *cli.Command {
|
func (mfa *CLIApp) fetchCommand() *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: cmdFetch,
|
Name: cmdFetch,
|
||||||
Usage: "fetch manifest and referenced files",
|
Usage: "fetch manifest and referenced files",
|
||||||
ArgsUsage: "URL",
|
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
mfa.setVerbosity(c)
|
mfa.setVerbosity(c)
|
||||||
mfa.printBanner()
|
mfa.printBanner()
|
||||||
|
|||||||
+4
-17
@@ -35,8 +35,7 @@ var (
|
|||||||
errPathDotDot = errors.New("contains '..' segment")
|
errPathDotDot = errors.New("contains '..' segment")
|
||||||
errSizeMismatch = errors.New("size mismatch")
|
errSizeMismatch = errors.New("size mismatch")
|
||||||
errNegativeSize = errors.New("size cannot be negative")
|
errNegativeSize = errors.New("size cannot be negative")
|
||||||
errHashNotMultihash = errors.New("hash is not a valid multihash")
|
errEmptyHash = errors.New("hash cannot be nil or empty")
|
||||||
errHashTooShort = errors.New("hash digest is too short")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// ValidatePath checks that a file path conforms to manifest path invariants:
|
// ValidatePath checks that a file path conforms to manifest path invariants:
|
||||||
@@ -229,8 +228,7 @@ func (b *Builder) FileCount() int {
|
|||||||
|
|
||||||
// AddFileWithHash adds a file entry with a pre-computed hash.
|
// AddFileWithHash adds a file entry with a pre-computed hash.
|
||||||
// This is useful when the hash is already known (e.g., from an existing manifest).
|
// This is useful when the hash is already known (e.g., from an existing manifest).
|
||||||
// Returns an error if path is invalid, size is negative, or hash is not a
|
// Returns an error if path is empty, size is negative, or hash is nil/empty.
|
||||||
// multihash with a digest of at least 32 bytes, as long as SHA-256's.
|
|
||||||
func (b *Builder) AddFileWithHash(
|
func (b *Builder) AddFileWithHash(
|
||||||
path RelFilePath,
|
path RelFilePath,
|
||||||
size FileSize,
|
size FileSize,
|
||||||
@@ -246,19 +244,8 @@ func (b *Builder) AddFileWithHash(
|
|||||||
return errNegativeSize
|
return errNegativeSize
|
||||||
}
|
}
|
||||||
|
|
||||||
decoded, err := multihash.Decode(hash)
|
if len(hash) == 0 {
|
||||||
if err != nil {
|
return errEmptyHash
|
||||||
return fmt.Errorf("%w: %w", errHashNotMultihash, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The reader's limit on decoding cost (maxDecodedGrowth) assumes every
|
|
||||||
// hash is at least as long as a SHA-256 multihash, so a manifest of
|
|
||||||
// shorter ones could fail to load.
|
|
||||||
if len(decoded.Digest) < sha256.Size {
|
|
||||||
return fmt.Errorf(
|
|
||||||
"%w: %d bytes, at least %d needed",
|
|
||||||
errHashTooShort, len(decoded.Digest), sha256.Size,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
entry := &MFFilePath{
|
entry := &MFFilePath{
|
||||||
|
|||||||
+27
-70
@@ -4,15 +4,11 @@ package mfer
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/multiformats/go-multihash"
|
|
||||||
"github.com/spf13/afero"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
@@ -46,10 +42,9 @@ func TestBuilderAddFileWithHash(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
b := NewBuilder()
|
b := NewBuilder()
|
||||||
hash, err := multihash.Encode(make([]byte, sha256.Size), multihash.SHA2_256)
|
hash := make([]byte, 34) // SHA256 multihash is 34 bytes
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
err = b.AddFileWithHash("test.txt", 100, ModTime(time.Now()), hash)
|
err := b.AddFileWithHash("test.txt", 100, ModTime(time.Now()), hash)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 1, b.FileCount())
|
assert.Equal(t, 1, b.FileCount())
|
||||||
}
|
}
|
||||||
@@ -57,14 +52,12 @@ func TestBuilderAddFileWithHash(t *testing.T) {
|
|||||||
func TestBuilderAddFileWithHashValidation(t *testing.T) {
|
func TestBuilderAddFileWithHashValidation(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
sha256Hash, err := multihash.Encode(make([]byte, sha256.Size), multihash.SHA2_256)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
t.Run("empty path", func(t *testing.T) {
|
t.Run("empty path", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
b := NewBuilder()
|
b := NewBuilder()
|
||||||
err := b.AddFileWithHash("", 100, ModTime(time.Now()), sha256Hash)
|
hash := make([]byte, 34)
|
||||||
|
err := b.AddFileWithHash("", 100, ModTime(time.Now()), hash)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "path")
|
assert.Contains(t, err.Error(), "path")
|
||||||
})
|
})
|
||||||
@@ -73,53 +66,41 @@ func TestBuilderAddFileWithHashValidation(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
b := NewBuilder()
|
b := NewBuilder()
|
||||||
err := b.AddFileWithHash("test.txt", -1, ModTime(time.Now()), sha256Hash)
|
hash := make([]byte, 34)
|
||||||
|
err := b.AddFileWithHash("test.txt", -1, ModTime(time.Now()), hash)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "size")
|
assert.Contains(t, err.Error(), "size")
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("nil hash", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := NewBuilder()
|
||||||
|
err := b.AddFileWithHash("test.txt", 100, ModTime(time.Now()), nil)
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "hash")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("empty hash", func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := NewBuilder()
|
||||||
|
err := b.AddFileWithHash("test.txt", 100, ModTime(time.Now()), []byte{})
|
||||||
|
require.Error(t, err)
|
||||||
|
assert.Contains(t, err.Error(), "hash")
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("valid inputs", func(t *testing.T) {
|
t.Run("valid inputs", func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
b := NewBuilder()
|
b := NewBuilder()
|
||||||
err := b.AddFileWithHash("test.txt", 100, ModTime(time.Now()), sha256Hash)
|
hash := make([]byte, 34)
|
||||||
|
err := b.AddFileWithHash("test.txt", 100, ModTime(time.Now()), hash)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, 1, b.FileCount())
|
assert.Equal(t, 1, b.FileCount())
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuilderAddFileWithHashRejectsBadHashes(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
sha1Hash, err := multihash.Encode(make([]byte, 20), multihash.SHA1)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
hash Multihash
|
|
||||||
want error
|
|
||||||
}{
|
|
||||||
{"nil hash", nil, errHashNotMultihash},
|
|
||||||
{"empty hash", []byte{}, errHashNotMultihash},
|
|
||||||
{"one-byte hash", []byte{0x12}, errHashNotMultihash},
|
|
||||||
// A SHA-256 code and 32-byte length, then only two bytes of digest.
|
|
||||||
{"malformed multihash", []byte{0x12, 0x20, 0x01, 0x02}, errHashNotMultihash},
|
|
||||||
// A valid multihash, but its 20-byte SHA-1 digest is too short.
|
|
||||||
{"SHA-1 multihash", sha1Hash, errHashTooShort},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
b := NewBuilder()
|
|
||||||
err := b.AddFileWithHash("test.txt", 100, ModTime(time.Now()), tt.hash)
|
|
||||||
require.ErrorIs(t, err, tt.want)
|
|
||||||
assert.Equal(t, 0, b.FileCount())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuilderBuild(t *testing.T) {
|
func TestBuilderBuild(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -375,8 +356,7 @@ func TestBuilderBuildRoundTrip(t *testing.T) {
|
|||||||
func TestBuilderBuildRoundTripLargeManifest(t *testing.T) {
|
func TestBuilderBuildRoundTripLargeManifest(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
hash, err := multihash.Encode(make([]byte, sha256.Size), multihash.SHA2_256)
|
hash := make([]byte, 34) // multihash: 2-byte prefix + 32-byte SHA-256
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
b := NewBuilder()
|
b := NewBuilder()
|
||||||
|
|
||||||
@@ -417,29 +397,6 @@ func TestNewManifestFromReaderTruncated(t *testing.T) {
|
|||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewManifestFromFileRequiresPath(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
_, err := NewManifestFromFile(nil)
|
|
||||||
require.ErrorIs(t, err, errManifestPathEmpty)
|
|
||||||
|
|
||||||
_, err = NewManifestFromFile(&ManifestFromFileOptions{Fs: afero.NewMemMapFs()})
|
|
||||||
require.ErrorIs(t, err, errManifestPathEmpty)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewManifestFromFileNilFsUsesOsFs(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
path := filepath.Join(t.TempDir(), "index.mf")
|
|
||||||
createTestManifest(t, afero.NewOsFs(), path, map[string][]byte{
|
|
||||||
testFileName: []byte("hello"),
|
|
||||||
})
|
|
||||||
|
|
||||||
m, err := NewManifestFromFile(&ManifestFromFileOptions{Path: path})
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Len(t, m.Files(), 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestManifestString(t *testing.T) {
|
func TestManifestString(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
+8
-34
@@ -14,11 +14,7 @@ import (
|
|||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var errNoSigningPubKey = errors.New("manifest has no signing public key")
|
||||||
errNoSigningPubKey = errors.New("manifest has no signing public key")
|
|
||||||
errManifestPathEmpty = errors.New("manifest path cannot be empty")
|
|
||||||
errBasePathEmpty = errors.New("base path cannot be empty")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Result represents the outcome of checking a single file.
|
// Result represents the outcome of checking a single file.
|
||||||
type Result struct {
|
type Result struct {
|
||||||
@@ -86,42 +82,20 @@ type Checker struct {
|
|||||||
signingPubKey []byte
|
signingPubKey []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckerOptions configures a Checker.
|
// NewChecker creates a new Checker for the given manifest, base path, and filesystem.
|
||||||
type CheckerOptions struct {
|
// The basePath is the directory relative to which manifest paths are resolved.
|
||||||
// ManifestPath is the manifest file to check against (required).
|
// If fs is nil, the real filesystem (OsFs) is used.
|
||||||
ManifestPath string
|
func NewChecker(manifestPath string, basePath string, fs afero.Fs) (*Checker, error) {
|
||||||
// BasePath is the directory relative to which manifest paths are
|
|
||||||
// resolved (required).
|
|
||||||
BasePath string
|
|
||||||
// Fs is the filesystem to use, defaults to OsFs if nil.
|
|
||||||
Fs afero.Fs
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewChecker creates a new Checker with the given options. It returns an
|
|
||||||
// error if opts is nil or either path is empty.
|
|
||||||
func NewChecker(opts *CheckerOptions) (*Checker, error) {
|
|
||||||
if opts == nil || opts.ManifestPath == "" {
|
|
||||||
return nil, errManifestPathEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.BasePath == "" {
|
|
||||||
return nil, errBasePathEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
fs := opts.Fs
|
|
||||||
if fs == nil {
|
if fs == nil {
|
||||||
fs = afero.NewOsFs()
|
fs = afero.NewOsFs()
|
||||||
}
|
}
|
||||||
|
|
||||||
m, err := NewManifestFromFile(&ManifestFromFileOptions{
|
m, err := NewManifestFromFile(fs, manifestPath)
|
||||||
Path: opts.ManifestPath,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
abs, err := filepath.Abs(opts.BasePath)
|
abs, err := filepath.Abs(basePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -134,7 +108,7 @@ func NewChecker(opts *CheckerOptions) (*Checker, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compute manifest's relative path from basePath for exclusion in FindExtraFiles
|
// Compute manifest's relative path from basePath for exclusion in FindExtraFiles
|
||||||
absManifest, err := filepath.Abs(opts.ManifestPath)
|
absManifest, err := filepath.Abs(manifestPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-190
@@ -5,8 +5,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -16,11 +14,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
testFile1 = "file1.txt"
|
testFile1 = "file1.txt"
|
||||||
testFile2 = "file2.txt"
|
testFile2 = "file2.txt"
|
||||||
testExistsFile = "exists.txt"
|
testExistsFile = "exists.txt"
|
||||||
testManifestPath = "/manifest.mf"
|
|
||||||
testDataDir = "/data"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStatusString(t *testing.T) {
|
func TestStatusString(t *testing.T) {
|
||||||
@@ -70,13 +66,15 @@ func createTestManifest(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// createFilesOnDisk creates the given files on the filesystem under
|
// createFilesOnDisk creates the given files on the filesystem under
|
||||||
// testDataDir.
|
// /data.
|
||||||
func createFilesOnDisk(t *testing.T, fs afero.Fs, files map[string][]byte) {
|
func createFilesOnDisk(t *testing.T, fs afero.Fs, files map[string][]byte) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
basePath := "/data"
|
||||||
|
|
||||||
for path, content := range files {
|
for path, content := range files {
|
||||||
fullPath := testDataDir + "/" + path
|
fullPath := basePath + "/" + path
|
||||||
require.NoError(t, fs.MkdirAll(testDataDir, 0o755))
|
require.NoError(t, fs.MkdirAll(basePath, 0o755))
|
||||||
require.NoError(t, afero.WriteFile(fs, fullPath, content, 0o644))
|
require.NoError(t, afero.WriteFile(fs, fullPath, content, 0o644))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,13 +90,9 @@ func TestNewChecker(t *testing.T) {
|
|||||||
testFile1: []byte("hello"),
|
testFile1: []byte("hello"),
|
||||||
testFile2: []byte("world"),
|
testFile2: []byte("world"),
|
||||||
}
|
}
|
||||||
createTestManifest(t, fs, testManifestPath, files)
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{
|
chk, err := NewChecker("/manifest.mf", "/", fs)
|
||||||
ManifestPath: testManifestPath,
|
|
||||||
BasePath: "/",
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.NotNil(t, chk)
|
assert.NotNil(t, chk)
|
||||||
assert.Equal(t, FileCount(2), chk.FileCount())
|
assert.Equal(t, FileCount(2), chk.FileCount())
|
||||||
@@ -108,11 +102,7 @@ func TestNewChecker(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
_, err := NewChecker(&CheckerOptions{
|
_, err := NewChecker("/nonexistent.mf", "/", fs)
|
||||||
ManifestPath: "/nonexistent.mf",
|
|
||||||
BasePath: "/",
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -121,73 +111,11 @@ func TestNewChecker(t *testing.T) {
|
|||||||
|
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
require.NoError(t, afero.WriteFile(fs, "/bad.mf", []byte("not a manifest"), 0o644))
|
require.NoError(t, afero.WriteFile(fs, "/bad.mf", []byte("not a manifest"), 0o644))
|
||||||
_, err := NewChecker(&CheckerOptions{
|
_, err := NewChecker("/bad.mf", "/", fs)
|
||||||
ManifestPath: "/bad.mf",
|
|
||||||
BasePath: "/",
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNewCheckerRequiredPaths(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
for _, tc := range []struct {
|
|
||||||
name string
|
|
||||||
opts *CheckerOptions
|
|
||||||
want string
|
|
||||||
is error
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "nil options",
|
|
||||||
opts: nil,
|
|
||||||
want: "manifest path cannot be empty",
|
|
||||||
is: errManifestPathEmpty,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty manifest path",
|
|
||||||
opts: &CheckerOptions{BasePath: testDataDir},
|
|
||||||
want: "manifest path cannot be empty",
|
|
||||||
is: errManifestPathEmpty,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty base path",
|
|
||||||
opts: &CheckerOptions{ManifestPath: testManifestPath},
|
|
||||||
want: "base path cannot be empty",
|
|
||||||
is: errBasePathEmpty,
|
|
||||||
},
|
|
||||||
} {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
chk, err := NewChecker(tc.opts)
|
|
||||||
require.ErrorIs(t, err, tc.is)
|
|
||||||
require.EqualError(t, err, tc.want)
|
|
||||||
assert.Nil(t, chk)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewCheckerNilFsUsesOsFs(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
dir := t.TempDir()
|
|
||||||
manifestPath := filepath.Join(dir, "index.mf")
|
|
||||||
content := []byte("hello")
|
|
||||||
createTestManifest(t, afero.NewOsFs(), manifestPath, map[string][]byte{
|
|
||||||
testFile1: content,
|
|
||||||
})
|
|
||||||
require.NoError(t, os.WriteFile(filepath.Join(dir, testFile1), content, 0o600))
|
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{ManifestPath: manifestPath, BasePath: dir})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
results := make(chan Result, 1)
|
|
||||||
require.NoError(t, chk.Check(context.Background(), results, nil))
|
|
||||||
assert.Equal(t, StatusOK, (<-results).Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckerFileCountAndTotalBytes(t *testing.T) {
|
func TestCheckerFileCountAndTotalBytes(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -197,13 +125,9 @@ func TestCheckerFileCountAndTotalBytes(t *testing.T) {
|
|||||||
"medium.txt": []byte("hello world"),
|
"medium.txt": []byte("hello world"),
|
||||||
"large.txt": bytes.Repeat([]byte("x"), 1000),
|
"large.txt": bytes.Repeat([]byte("x"), 1000),
|
||||||
}
|
}
|
||||||
createTestManifest(t, fs, testManifestPath, files)
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{
|
chk, err := NewChecker("/manifest.mf", "/", fs)
|
||||||
ManifestPath: testManifestPath,
|
|
||||||
BasePath: "/",
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, FileCount(3), chk.FileCount())
|
assert.Equal(t, FileCount(3), chk.FileCount())
|
||||||
@@ -218,14 +142,10 @@ func TestCheckAllFilesOK(t *testing.T) {
|
|||||||
testFile1: []byte("content one"),
|
testFile1: []byte("content one"),
|
||||||
testFile2: []byte("content two"),
|
testFile2: []byte("content two"),
|
||||||
}
|
}
|
||||||
createTestManifest(t, fs, testManifestPath, files)
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
createFilesOnDisk(t, fs, files)
|
createFilesOnDisk(t, fs, files)
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
||||||
ManifestPath: testManifestPath,
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results := make(chan Result, 10)
|
results := make(chan Result, 10)
|
||||||
@@ -252,17 +172,13 @@ func TestCheckMissingFile(t *testing.T) {
|
|||||||
testExistsFile: []byte("I exist"),
|
testExistsFile: []byte("I exist"),
|
||||||
"missing.txt": []byte("I don't exist on disk"),
|
"missing.txt": []byte("I don't exist on disk"),
|
||||||
}
|
}
|
||||||
createTestManifest(t, fs, testManifestPath, files)
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
// Only create one file
|
// Only create one file
|
||||||
createFilesOnDisk(t, fs, map[string][]byte{
|
createFilesOnDisk(t, fs, map[string][]byte{
|
||||||
testExistsFile: []byte("I exist"),
|
testExistsFile: []byte("I exist"),
|
||||||
})
|
})
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
||||||
ManifestPath: testManifestPath,
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results := make(chan Result, 10)
|
results := make(chan Result, 10)
|
||||||
@@ -295,17 +211,13 @@ func TestCheckSizeMismatch(t *testing.T) {
|
|||||||
files := map[string][]byte{
|
files := map[string][]byte{
|
||||||
testFileName: []byte("original content"),
|
testFileName: []byte("original content"),
|
||||||
}
|
}
|
||||||
createTestManifest(t, fs, testManifestPath, files)
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
// Create file with different size
|
// Create file with different size
|
||||||
createFilesOnDisk(t, fs, map[string][]byte{
|
createFilesOnDisk(t, fs, map[string][]byte{
|
||||||
testFileName: []byte("short"),
|
testFileName: []byte("short"),
|
||||||
})
|
})
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
||||||
ManifestPath: testManifestPath,
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results := make(chan Result, 10)
|
results := make(chan Result, 10)
|
||||||
@@ -325,7 +237,7 @@ func TestCheckHashMismatch(t *testing.T) {
|
|||||||
files := map[string][]byte{
|
files := map[string][]byte{
|
||||||
testFileName: originalContent,
|
testFileName: originalContent,
|
||||||
}
|
}
|
||||||
createTestManifest(t, fs, testManifestPath, files)
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
// Create file with same size but different content
|
// Create file with same size but different content
|
||||||
differentContent := []byte("different contnt") // same length (16 bytes) but different
|
differentContent := []byte("different contnt") // same length (16 bytes) but different
|
||||||
require.Len(t, differentContent, len(originalContent), "test requires same length")
|
require.Len(t, differentContent, len(originalContent), "test requires same length")
|
||||||
@@ -333,11 +245,7 @@ func TestCheckHashMismatch(t *testing.T) {
|
|||||||
testFileName: differentContent,
|
testFileName: differentContent,
|
||||||
})
|
})
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
||||||
ManifestPath: testManifestPath,
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results := make(chan Result, 10)
|
results := make(chan Result, 10)
|
||||||
@@ -357,14 +265,10 @@ func TestCheckWithProgress(t *testing.T) {
|
|||||||
testFile1: bytes.Repeat([]byte("a"), 100),
|
testFile1: bytes.Repeat([]byte("a"), 100),
|
||||||
testFile2: bytes.Repeat([]byte("b"), 200),
|
testFile2: bytes.Repeat([]byte("b"), 200),
|
||||||
}
|
}
|
||||||
createTestManifest(t, fs, testManifestPath, files)
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
createFilesOnDisk(t, fs, files)
|
createFilesOnDisk(t, fs, files)
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
||||||
ManifestPath: testManifestPath,
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results := make(chan Result, 10)
|
results := make(chan Result, 10)
|
||||||
@@ -401,14 +305,10 @@ func TestCheckContextCancellation(t *testing.T) {
|
|||||||
files[string(rune('a'+i%26))+".txt"] = bytes.Repeat([]byte("x"), 1000)
|
files[string(rune('a'+i%26))+".txt"] = bytes.Repeat([]byte("x"), 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
createTestManifest(t, fs, testManifestPath, files)
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
createFilesOnDisk(t, fs, files)
|
createFilesOnDisk(t, fs, files)
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
||||||
ManifestPath: testManifestPath,
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
@@ -427,7 +327,7 @@ func TestFindExtraFiles(t *testing.T) {
|
|||||||
manifestFiles := map[string][]byte{
|
manifestFiles := map[string][]byte{
|
||||||
testFile1: []byte("in manifest"),
|
testFile1: []byte("in manifest"),
|
||||||
}
|
}
|
||||||
createTestManifest(t, fs, testManifestPath, manifestFiles)
|
createTestManifest(t, fs, "/manifest.mf", manifestFiles)
|
||||||
|
|
||||||
// Disk has file1 and file2
|
// Disk has file1 and file2
|
||||||
createFilesOnDisk(t, fs, map[string][]byte{
|
createFilesOnDisk(t, fs, map[string][]byte{
|
||||||
@@ -435,11 +335,7 @@ func TestFindExtraFiles(t *testing.T) {
|
|||||||
testFile2: []byte("extra file"),
|
testFile2: []byte("extra file"),
|
||||||
})
|
})
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
||||||
ManifestPath: testManifestPath,
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results := make(chan Result, 10)
|
results := make(chan Result, 10)
|
||||||
@@ -472,14 +368,10 @@ func TestFindExtraFilesSkipsManifestAndDotfiles(t *testing.T) {
|
|||||||
require.NoError(t, afero.WriteFile(fs, "/data/.hidden", []byte("hidden"), 0o644))
|
require.NoError(t, afero.WriteFile(fs, "/data/.hidden", []byte("hidden"), 0o644))
|
||||||
require.NoError(t, afero.WriteFile(fs, "/data/.config/settings", []byte("cfg"), 0o644))
|
require.NoError(t, afero.WriteFile(fs, "/data/.config/settings", []byte("cfg"), 0o644))
|
||||||
// Create a real extra file
|
// Create a real extra file
|
||||||
require.NoError(t, fs.MkdirAll(testDataDir, 0o755))
|
require.NoError(t, fs.MkdirAll("/data", 0o755))
|
||||||
require.NoError(t, afero.WriteFile(fs, "/data/extra.txt", []byte("extra"), 0o644))
|
require.NoError(t, afero.WriteFile(fs, "/data/extra.txt", []byte("extra"), 0o644))
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{
|
chk, err := NewChecker("/data/.index.mf", "/data", fs)
|
||||||
ManifestPath: "/data/.index.mf",
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results := make(chan Result, 10)
|
results := make(chan Result, 10)
|
||||||
@@ -508,14 +400,10 @@ func TestFindExtraFilesContextCancellation(t *testing.T) {
|
|||||||
|
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
files := map[string][]byte{testFileName: []byte("data")}
|
files := map[string][]byte{testFileName: []byte("data")}
|
||||||
createTestManifest(t, fs, testManifestPath, files)
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
createFilesOnDisk(t, fs, files)
|
createFilesOnDisk(t, fs, files)
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
||||||
ManifestPath: testManifestPath,
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
@@ -531,14 +419,10 @@ func TestCheckNilChannels(t *testing.T) {
|
|||||||
|
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
files := map[string][]byte{testFileName: []byte("data")}
|
files := map[string][]byte{testFileName: []byte("data")}
|
||||||
createTestManifest(t, fs, testManifestPath, files)
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
createFilesOnDisk(t, fs, files)
|
createFilesOnDisk(t, fs, files)
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
||||||
ManifestPath: testManifestPath,
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Should not panic with nil channels
|
// Should not panic with nil channels
|
||||||
@@ -551,14 +435,10 @@ func TestFindExtraFilesNilChannel(t *testing.T) {
|
|||||||
|
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
files := map[string][]byte{testFileName: []byte("data")}
|
files := map[string][]byte{testFileName: []byte("data")}
|
||||||
createTestManifest(t, fs, testManifestPath, files)
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
createFilesOnDisk(t, fs, files)
|
createFilesOnDisk(t, fs, files)
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
||||||
ManifestPath: testManifestPath,
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Should not panic with nil channel
|
// Should not panic with nil channel
|
||||||
@@ -575,7 +455,7 @@ func TestCheckSubdirectories(t *testing.T) {
|
|||||||
"dir1/dir2/file2.txt": []byte("content2"),
|
"dir1/dir2/file2.txt": []byte("content2"),
|
||||||
"dir1/dir2/dir3/deep.txt": []byte("deep content"),
|
"dir1/dir2/dir3/deep.txt": []byte("deep content"),
|
||||||
}
|
}
|
||||||
createTestManifest(t, fs, testManifestPath, files)
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
|
|
||||||
// Create files with full directory structure
|
// Create files with full directory structure
|
||||||
for path, content := range files {
|
for path, content := range files {
|
||||||
@@ -585,11 +465,7 @@ func TestCheckSubdirectories(t *testing.T) {
|
|||||||
require.NoError(t, afero.WriteFile(fs, fullPath, content, 0o644))
|
require.NoError(t, afero.WriteFile(fs, fullPath, content, 0o644))
|
||||||
}
|
}
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
||||||
ManifestPath: testManifestPath,
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results := make(chan Result, 10)
|
results := make(chan Result, 10)
|
||||||
@@ -617,17 +493,13 @@ func TestCheckMissingFileDetectedWithoutFallback(t *testing.T) {
|
|||||||
testExistsFile: []byte("here"),
|
testExistsFile: []byte("here"),
|
||||||
"missing.txt": []byte("not on disk"),
|
"missing.txt": []byte("not on disk"),
|
||||||
}
|
}
|
||||||
createTestManifest(t, fs, testManifestPath, files)
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
// Only create one file on disk
|
// Only create one file on disk
|
||||||
createFilesOnDisk(t, fs, map[string][]byte{
|
createFilesOnDisk(t, fs, map[string][]byte{
|
||||||
testExistsFile: []byte("here"),
|
testExistsFile: []byte("here"),
|
||||||
})
|
})
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
||||||
ManifestPath: testManifestPath,
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results := make(chan Result, 10)
|
results := make(chan Result, 10)
|
||||||
@@ -665,11 +537,7 @@ func TestFindExtraFilesSkipsDotfiles(t *testing.T) {
|
|||||||
require.NoError(t,
|
require.NoError(t,
|
||||||
afero.WriteFile(fs, "/data/.git/config", []byte("git config"), 0o644))
|
afero.WriteFile(fs, "/data/.git/config", []byte("git config"), 0o644))
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{
|
chk, err := NewChecker("/data/.index.mf", "/data", fs)
|
||||||
ManifestPath: "/data/.index.mf",
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results := make(chan Result, 10)
|
results := make(chan Result, 10)
|
||||||
@@ -698,11 +566,7 @@ func TestFindExtraFilesSkipsManifestFile(t *testing.T) {
|
|||||||
createTestManifest(t, fs, "/data/index.mf", files)
|
createTestManifest(t, fs, "/data/index.mf", files)
|
||||||
createFilesOnDisk(t, fs, files)
|
createFilesOnDisk(t, fs, files)
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{
|
chk, err := NewChecker("/data/index.mf", "/data", fs)
|
||||||
ManifestPath: "/data/index.mf",
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results := make(chan Result, 10)
|
results := make(chan Result, 10)
|
||||||
@@ -723,13 +587,9 @@ func TestCheckEmptyManifest(t *testing.T) {
|
|||||||
|
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
// Create manifest with no files
|
// Create manifest with no files
|
||||||
createTestManifest(t, fs, testManifestPath, map[string][]byte{})
|
createTestManifest(t, fs, "/manifest.mf", map[string][]byte{})
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
||||||
ManifestPath: testManifestPath,
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
assert.Equal(t, FileCount(0), chk.FileCount())
|
assert.Equal(t, FileCount(0), chk.FileCount())
|
||||||
@@ -761,14 +621,10 @@ func TestCheckProgressRateLimited(t *testing.T) {
|
|||||||
files[name] = []byte("content")
|
files[name] = []byte("content")
|
||||||
}
|
}
|
||||||
|
|
||||||
createTestManifest(t, fs, testManifestPath, files)
|
createTestManifest(t, fs, "/manifest.mf", files)
|
||||||
createFilesOnDisk(t, fs, files)
|
createFilesOnDisk(t, fs, files)
|
||||||
|
|
||||||
chk, err := NewChecker(&CheckerOptions{
|
chk, err := NewChecker("/manifest.mf", "/data", fs)
|
||||||
ManifestPath: testManifestPath,
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results := make(chan Result, 200)
|
results := make(chan Result, 200)
|
||||||
|
|||||||
@@ -17,28 +17,4 @@ const (
|
|||||||
|
|
||||||
// uuidLength is the length in bytes of a binary UUID.
|
// uuidLength is the length in bytes of a binary UUID.
|
||||||
uuidLength = 16
|
uuidLength = 16
|
||||||
|
|
||||||
// Numbers in mf.proto of MFFile.files and of the MFFilePath fields
|
|
||||||
// that decoding sets aside a fixed amount of memory for.
|
|
||||||
filesFieldNumber = 101
|
|
||||||
hashesFieldNumber = 3
|
|
||||||
mimeTypeFieldNumber = 301
|
|
||||||
mtimeFieldNumber = 302
|
|
||||||
ctimeFieldNumber = 303
|
|
||||||
|
|
||||||
// Bytes decoding sets aside for each file entry, hash, timestamp and
|
|
||||||
// MIME type, however short its encoding. checkDecodedSize refuses an
|
|
||||||
// inner message for which these add up to more than maxDecodedGrowth
|
|
||||||
// times its size.
|
|
||||||
decodedFileEntrySize = 160
|
|
||||||
decodedHashSize = 112
|
|
||||||
decodedTimestampSize = 64
|
|
||||||
decodedMIMETypeSize = 16
|
|
||||||
|
|
||||||
// Each file entry mfer writes holds a path of at least one byte, a
|
|
||||||
// multihash at least as long as SHA-256's 34 bytes (AddFileWithHash
|
|
||||||
// refuses shorter ones) and a modification time: at least 47 bytes,
|
|
||||||
// counted at 336. So its manifests add up to at most about 7.15 times
|
|
||||||
// their size, and this limit is about 12% above that.
|
|
||||||
maxDecodedGrowth = 8
|
|
||||||
)
|
)
|
||||||
|
|||||||
+6
-108
@@ -11,7 +11,6 @@ import (
|
|||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/klauspost/compress/zstd"
|
"github.com/klauspost/compress/zstd"
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"google.golang.org/protobuf/encoding/protowire"
|
|
||||||
"google.golang.org/protobuf/proto"
|
"google.golang.org/protobuf/proto"
|
||||||
"sneak.berlin/go/mfer/internal/bork"
|
"sneak.berlin/go/mfer/internal/bork"
|
||||||
"sneak.berlin/go/mfer/internal/log"
|
"sneak.berlin/go/mfer/internal/log"
|
||||||
@@ -28,8 +27,6 @@ var (
|
|||||||
errUUIDMismatch = errors.New("outer and inner UUID mismatch")
|
errUUIDMismatch = errors.New("outer and inner UUID mismatch")
|
||||||
errInvalidFileFormat = errors.New("invalid file format")
|
errInvalidFileFormat = errors.New("invalid file format")
|
||||||
errInvalidManifestPath = errors.New("manifest contains invalid path")
|
errInvalidManifestPath = errors.New("manifest contains invalid path")
|
||||||
errDecodedTooLarge = errors.New(
|
|
||||||
"manifest would take too much memory to decode")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// validateUUID checks that the byte slice is a valid UUID (16 bytes, parseable).
|
// validateUUID checks that the byte slice is a valid UUID (16 bytes, parseable).
|
||||||
@@ -157,85 +154,6 @@ func (m *manifest) decompressInner() ([]byte, error) {
|
|||||||
return dat, nil
|
return dat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkDecodedSize refuses an encoded inner message whose file entries,
|
|
||||||
// hashes, timestamps and MIME types would take more than maxDecodedGrowth
|
|
||||||
// times its size to decode. Decoding sets aside a fixed amount for each,
|
|
||||||
// however short its encoding, so a message of empty ones would take about
|
|
||||||
// 50 times its size.
|
|
||||||
func checkDecodedSize(inner []byte) error {
|
|
||||||
limit := maxDecodedGrowth * int64(len(inner))
|
|
||||||
|
|
||||||
var decoded int64
|
|
||||||
|
|
||||||
add := func(size int64) error {
|
|
||||||
decoded += size
|
|
||||||
if decoded > limit {
|
|
||||||
return errDecodedTooLarge
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return forEachBytesField(inner, func(num protowire.Number, entry []byte) error {
|
|
||||||
if num != filesFieldNumber {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
err := add(decodedFileEntrySize)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return forEachBytesField(entry, func(num protowire.Number, _ []byte) error {
|
|
||||||
if num == hashesFieldNumber {
|
|
||||||
return add(decodedHashSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
if num == mtimeFieldNumber || num == ctimeFieldNumber {
|
|
||||||
return add(decodedTimestampSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
if num == mimeTypeFieldNumber {
|
|
||||||
return add(decodedMIMETypeSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// forEachBytesField calls fn with the number and value of each
|
|
||||||
// length-delimited field in the encoded message msg, and fails if msg is
|
|
||||||
// malformed.
|
|
||||||
func forEachBytesField(
|
|
||||||
msg []byte, fn func(num protowire.Number, value []byte) error,
|
|
||||||
) error {
|
|
||||||
for len(msg) > 0 {
|
|
||||||
num, wireType, tagLen := protowire.ConsumeTag(msg)
|
|
||||||
if tagLen < 0 {
|
|
||||||
return protowire.ParseError(tagLen)
|
|
||||||
}
|
|
||||||
|
|
||||||
valueLen := protowire.ConsumeFieldValue(num, wireType, msg[tagLen:])
|
|
||||||
if valueLen < 0 {
|
|
||||||
return protowire.ParseError(valueLen)
|
|
||||||
}
|
|
||||||
|
|
||||||
if wireType == protowire.BytesType {
|
|
||||||
value, _ := protowire.ConsumeBytes(msg[tagLen:])
|
|
||||||
|
|
||||||
err := fn(num, value)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
msg = msg[tagLen+valueLen:]
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *manifest) deserializeInner() error {
|
func (m *manifest) deserializeInner() error {
|
||||||
err := m.validateOuterHeader()
|
err := m.validateOuterHeader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -259,16 +177,10 @@ func (m *manifest) deserializeInner() error {
|
|||||||
return bork.ErrFileTruncated
|
return bork.ErrFileTruncated
|
||||||
}
|
}
|
||||||
|
|
||||||
err = checkDecodedSize(dat)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("deserialize: unmarshal inner: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deserialize inner message
|
// Deserialize inner message
|
||||||
m.pbInner = new(MFFile)
|
m.pbInner = new(MFFile)
|
||||||
|
|
||||||
// Unknown fields would cost memory; mfer never writes a loaded manifest out.
|
err = proto.Unmarshal(dat, m.pbInner)
|
||||||
err = proto.UnmarshalOptions{DiscardUnknown: true}.Unmarshal(dat, m.pbInner)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("deserialize: unmarshal inner: %w", err)
|
return fmt.Errorf("deserialize: unmarshal inner: %w", err)
|
||||||
}
|
}
|
||||||
@@ -337,8 +249,7 @@ func NewManifestFromReader(input io.Reader) (*manifest, error) {
|
|||||||
// deserialize outer:
|
// deserialize outer:
|
||||||
m.pbOuter = new(MFFileOuter)
|
m.pbOuter = new(MFFileOuter)
|
||||||
|
|
||||||
// Unknown fields would cost memory; mfer never writes a loaded manifest out.
|
err = proto.Unmarshal(dat, m.pbOuter)
|
||||||
err = proto.UnmarshalOptions{DiscardUnknown: true}.Unmarshal(dat, m.pbOuter)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -352,29 +263,16 @@ func NewManifestFromReader(input io.Reader) (*manifest, error) {
|
|||||||
return m, nil
|
return m, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ManifestFromFileOptions configures NewManifestFromFile.
|
// NewManifestFromFile reads a manifest from a file path using the given filesystem.
|
||||||
type ManifestFromFileOptions struct {
|
// If fs is nil, the real filesystem (OsFs) is used.
|
||||||
// Path is the manifest file to read (required).
|
|
||||||
Path string
|
|
||||||
// Fs is the filesystem to use, defaults to OsFs if nil.
|
|
||||||
Fs afero.Fs
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewManifestFromFile reads a manifest from a file. It returns an error if
|
|
||||||
// opts is nil or its path is empty.
|
|
||||||
//
|
//
|
||||||
//nolint:revive // unexported-return: exporting manifest is owner question 13
|
//nolint:revive // unexported-return: exporting manifest is owner question 13
|
||||||
func NewManifestFromFile(opts *ManifestFromFileOptions) (*manifest, error) {
|
func NewManifestFromFile(fs afero.Fs, path string) (*manifest, error) {
|
||||||
if opts == nil || opts.Path == "" {
|
|
||||||
return nil, errManifestPathEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
fs := opts.Fs
|
|
||||||
if fs == nil {
|
if fs == nil {
|
||||||
fs = afero.NewOsFs()
|
fs = afero.NewOsFs()
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := fs.Open(opts.Path)
|
f, err := fs.Open(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,15 +54,9 @@ func FuzzNewManifestFromReader(f *testing.F) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// It also keeps a few copies of its input. Buffers grow by
|
// It also keeps a few copies of its input. Buffers grow by
|
||||||
// copying, so reaching those sizes allocates up to about six times
|
// copying, so reaching those sizes allocates a few times them in
|
||||||
// them in total. Decoding the decompressed data takes up to
|
// total: sixteen times the input and the decompressed data leaves
|
||||||
// maxDecodedGrowth times its size for file entries, hashes,
|
// room for that.
|
||||||
// timestamps and MIME types, and drops fields it does not know.
|
|
||||||
// The strings and bytes it copies out of it, such as many one-byte
|
|
||||||
// values in one hash, take up to about five times more under the
|
|
||||||
// race detector, which pads every small copy to 16 bytes, and about
|
|
||||||
// half that without it. Twenty times the input and the
|
|
||||||
// decompressed data leaves room for all of that.
|
|
||||||
//
|
//
|
||||||
// The decoder also sets aside a new buffer of one to two times the
|
// The decoder also sets aside a new buffer of one to two times the
|
||||||
// window for each frame that asks for a larger window than the
|
// window for each frame that asks for a larger window than the
|
||||||
@@ -77,9 +71,8 @@ func FuzzNewManifestFromReader(f *testing.F) {
|
|||||||
// fails if the decoder accepts windows of twice zstdWindowSize; the
|
// fails if the decoder accepts windows of twice zstdWindowSize; the
|
||||||
// seed whose two frames together exceed MaxDecompressedSize fails
|
// seed whose two frames together exceed MaxDecompressedSize fails
|
||||||
// if the decoder decodes them in full instead of stopping at the
|
// if the decoder decodes them in full instead of stopping at the
|
||||||
// declared size; the seeds of empty file entries and of a file
|
// declared size.
|
||||||
// entry of empty hashes fail if the parser decodes them.
|
limit := 16*(uint64(len(data))+decompressed) + 24*zstdWindowSize
|
||||||
limit := 20*(uint64(len(data))+decompressed) + 24*zstdWindowSize
|
|
||||||
|
|
||||||
allocated := after.TotalAlloc - before.TotalAlloc
|
allocated := after.TotalAlloc - before.TotalAlloc
|
||||||
if allocated > limit {
|
if allocated > limit {
|
||||||
|
|||||||
@@ -6,14 +6,10 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"github.com/klauspost/compress/zstd"
|
"github.com/klauspost/compress/zstd"
|
||||||
"github.com/multiformats/go-multihash"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/protobuf/encoding/protowire"
|
"google.golang.org/protobuf/encoding/protowire"
|
||||||
@@ -118,119 +114,10 @@ func TestDeserializeRejectsInvalidEntryPaths(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Entries of a path, an empty hash, an empty MIME type and empty modification
|
|
||||||
// and change times are counted at 416 bytes each (160 + 112 + 16 + 64 + 64)
|
|
||||||
// and take 16 bytes plus the path to encode. A 35-character path makes that
|
|
||||||
// 51 bytes, about 8.2 times: refused, and leaving any one of the five
|
|
||||||
// uncounted, even the MIME type, brings it under 8. A 37-character path makes
|
|
||||||
// it 53 bytes, about 7.8 times: loaded.
|
|
||||||
func TestDeserializeRefusesEntriesThatDecodeTooLarge(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
pathLen int
|
|
||||||
refused bool
|
|
||||||
}{
|
|
||||||
{35, true},
|
|
||||||
{37, false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(strconv.Itoa(tt.pathLen), func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
entry := protowire.AppendTag(nil, 1, protowire.BytesType) // MFFilePath.path
|
|
||||||
entry = protowire.AppendString(entry, strings.Repeat("a", tt.pathLen))
|
|
||||||
entry = protowire.AppendTag(entry, 3, protowire.BytesType) // MFFilePath.hashes
|
|
||||||
entry = protowire.AppendBytes(entry, nil)
|
|
||||||
entry = protowire.AppendTag(entry, 301, protowire.BytesType) // MFFilePath.mimeType
|
|
||||||
entry = protowire.AppendBytes(entry, nil)
|
|
||||||
entry = protowire.AppendTag(entry, 302, protowire.BytesType) // MFFilePath.mtime
|
|
||||||
entry = protowire.AppendBytes(entry, nil)
|
|
||||||
entry = protowire.AppendTag(entry, 303, protowire.BytesType) // MFFilePath.ctime
|
|
||||||
entry = protowire.AppendBytes(entry, nil)
|
|
||||||
|
|
||||||
id := uuid.New()
|
|
||||||
inner := protowire.AppendTag(nil, 102, protowire.BytesType) // MFFile.uuid
|
|
||||||
inner = protowire.AppendBytes(inner, id[:])
|
|
||||||
|
|
||||||
for range 1000 {
|
|
||||||
inner = protowire.AppendTag(inner, 101, protowire.BytesType) // MFFile.files
|
|
||||||
inner = protowire.AppendBytes(inner, entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := NewManifestFromReader(bytes.NewReader(wrapInner(t, id, inner)))
|
|
||||||
if tt.refused {
|
|
||||||
require.ErrorIs(t, err, errDecodedTooLarge)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fields the decoder does not know are dropped, in the outer message, the inner
|
|
||||||
// message and a file entry, so that they take no memory once loaded.
|
|
||||||
func TestDeserializeDropsUnknownFields(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
unknown := protowire.AppendTag(nil, 99, protowire.BytesType) // in no message
|
|
||||||
unknown = protowire.AppendBytes(unknown, []byte("not known"))
|
|
||||||
|
|
||||||
entry := protowire.AppendTag(nil, 1, protowire.BytesType) // MFFilePath.path
|
|
||||||
entry = protowire.AppendString(entry, "a")
|
|
||||||
entry = append(entry, unknown...)
|
|
||||||
|
|
||||||
id := uuid.New()
|
|
||||||
inner := protowire.AppendTag(nil, 101, protowire.BytesType) // MFFile.files
|
|
||||||
inner = protowire.AppendBytes(inner, entry)
|
|
||||||
inner = protowire.AppendTag(inner, 102, protowire.BytesType) // MFFile.uuid
|
|
||||||
inner = protowire.AppendBytes(inner, id[:])
|
|
||||||
inner = append(inner, unknown...)
|
|
||||||
|
|
||||||
data := wrapInner(t, id, inner)
|
|
||||||
data = append(data, unknown...) // the outer message ends the file
|
|
||||||
|
|
||||||
m, err := NewManifestFromReader(bytes.NewReader(data))
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Len(t, m.Files(), 1)
|
|
||||||
assert.Empty(t, m.pbOuter.ProtoReflect().GetUnknown())
|
|
||||||
assert.Empty(t, m.pbInner.ProtoReflect().GetUnknown())
|
|
||||||
assert.Empty(t, m.Files()[0].ProtoReflect().GetUnknown())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Many empty files with names of at most three characters and modification
|
|
||||||
// times at the epoch make about the densest manifest mfer writes: it takes
|
|
||||||
// about 7 times its size to decode, and still loads. A signature would not
|
|
||||||
// change the inner message, so none is added.
|
|
||||||
func TestDeserializeLoadsDensestManifest(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
hash, err := multihash.Encode(make([]byte, sha256.Size), multihash.SHA2_256)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
b := NewBuilder()
|
|
||||||
b.SetIncludeTimestamps(true)
|
|
||||||
|
|
||||||
const files = 10000
|
|
||||||
for i := range files {
|
|
||||||
name := RelFilePath(strconv.FormatInt(int64(i), 36))
|
|
||||||
require.NoError(t, b.AddFileWithHash(name, 0, ModTime(time.Unix(0, 0)), hash))
|
|
||||||
}
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
require.NoError(t, b.Build(context.Background(), &buf))
|
|
||||||
|
|
||||||
m, err := NewManifestFromReader(&buf)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Len(t, m.Files(), files)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeserializeValidManifestRoundTrips(t *testing.T) {
|
func TestDeserializeValidManifestRoundTrips(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
hash, err := multihash.Encode(make([]byte, sha256.Size), multihash.SHA2_256)
|
hash := make([]byte, 34) // multihash: 2-byte prefix + 32-byte SHA-256
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
b := NewBuilder()
|
b := NewBuilder()
|
||||||
require.NoError(t, b.AddFileWithHash("dir/file.txt", 123, ModTime{}, hash))
|
require.NoError(t, b.AddFileWithHash("dir/file.txt", 123, ModTime{}, hash))
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
103901c42b94396aa7ae128fd503ef693a4b7a03b2169481f25fda3d2c254e00 mf.proto
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package mfer_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"encoding/hex"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// mf.pb.go is generated from mf.proto and committed. `make generate`
|
|
||||||
// records the hash of the mf.proto it generated from in mf.proto.sha256.
|
|
||||||
func TestGeneratedCodeMatchesProto(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
proto, err := os.ReadFile("mf.proto")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
recorded, err := os.ReadFile("mf.proto.sha256")
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
recordedHash, _, _ := strings.Cut(string(recorded), " ")
|
|
||||||
sum := sha256.Sum256(proto)
|
|
||||||
require.Equal(t, recordedHash, hex.EncodeToString(sum[:]),
|
|
||||||
"mfer/mf.proto has changed since mfer/mf.pb.go was generated "+
|
|
||||||
"from it: run `make generate` and commit the result")
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
[]byte("ZNAVSRFG\xa8\x06\x01\xb0\x06\x01\xb8\x06\xff\xff\xff\x03\xc2\x06 {\x16\xbdu\xa0\xa2\x11\xfcH\xef*\x1b7\r\x99\xefb\x04\x02g\n\xa9\xf3B5\xe5p\x96\x8c\x8c\xac\x0e\xca\x06\x10\x03Q\xb2\xd0\x19`F\xc1\xb1\xc0Z\xf4x\xf4g^\xba\f\xa1\x06(\xb5/\xfd\x04h\x04\x01\x00d\x01\xb2\x06\x10\x03Q\xb2\xd0\x19`F\xc1\xb1\xc0Z\xf4x\xf4g^\xaa\x06\x00\x01T\x13\x024\xce\xff\rL\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15M\x00\x00\x00\x01T\x00\x044\xfc\xff\x153\xea\a\xb4")
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
[]byte("ZNAVSRFG\xa8\x06\x01\xb0\x06\x01\xb8\x06\xff\xff\xff\x03\xc2\x06 .\xcd\x11|0\xfcP\xe5\x1b\xe3\xc6Ӡ\xcdڤx\xcd\x169t\x1a9~ǽB\xc9\xe8G`\x05\xca\x06\x10\x11*!\x0e\x95EF\xb8\xbd\x9f\xde\x12MF\r\x99\xba\f\xa6\x06(\xb5/\xfd\x04h,\x01\x00\xb4\x01\xb2\x06\x10\x11*!\x0e\x95EF\xb8\xbd\x9f\xde\x12MF\r\x99\xaa\x06\xe6\xff\xff\x03\x1a\x00\x01T\x14\x024\x8b\xff\x17L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15L\x00\x00\x00\x01T\x00\x044\xfd\xff\x15M\x00\x00\x00\x01T\x00\x044\xfc\xff\x15\x02\xd1.\xe3")
|
|
||||||
+11
@@ -5,8 +5,19 @@ set -eu
|
|||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||||
|
|
||||||
|
# Regenerate mfer/mf.pb.go from mfer/mf.proto if it is missing or stale
|
||||||
|
# (mirrors the old Makefile prerequisite; the generated file is
|
||||||
|
# committed, so this is normally a no-op).
|
||||||
|
ensure_pb() {
|
||||||
|
if [ ! -f mfer/mf.pb.go ] ||
|
||||||
|
[ -n "$(find mfer/mf.proto -newer mfer/mf.pb.go 2>/dev/null)" ]; then
|
||||||
|
(cd mfer && go generate .)
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
ensure_pb
|
||||||
gofumpt -l -w mfer internal cmd
|
gofumpt -l -w mfer internal cmd
|
||||||
# Markdown and JSON, over the same file set script/fmt-check verifies.
|
# Markdown and JSON, over the same file set script/fmt-check verifies.
|
||||||
"$SCRIPT_DIR/prettier" --write
|
"$SCRIPT_DIR/prettier" --write
|
||||||
|
|||||||
@@ -6,8 +6,19 @@ set -eu
|
|||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
# Regenerate mfer/mf.pb.go from mfer/mf.proto if it is missing or stale
|
||||||
|
# (mirrors the old Makefile prerequisite; the generated file is
|
||||||
|
# committed, so this is normally a no-op).
|
||||||
|
ensure_pb() {
|
||||||
|
if [ ! -f mfer/mf.pb.go ] ||
|
||||||
|
[ -n "$(find mfer/mf.proto -newer mfer/mf.pb.go 2>/dev/null)" ]; then
|
||||||
|
(cd mfer && go generate .)
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
ensure_pb
|
||||||
if [ -n "$(gofmt -l .)" ]; then
|
if [ -n "$(gofmt -l .)" ]; then
|
||||||
echo "gofmt: files need formatting:" >&2
|
echo "gofmt: files need formatting:" >&2
|
||||||
gofmt -l . >&2
|
gofmt -l . >&2
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# script/generate: regenerate mfer/mf.pb.go from mfer/mf.proto, and record
|
|
||||||
# the hash of that mf.proto in mfer/mf.proto.sha256. Nothing else
|
|
||||||
# regenerates mf.pb.go: it is committed, so building and checking need no
|
|
||||||
# protoc. A test fails while mf.proto no longer matches the recorded hash.
|
|
||||||
#
|
|
||||||
# Needs exactly the protoc and protoc-gen-go versions named in the header of
|
|
||||||
# the committed mf.pb.go (README.md says how to install them). Another
|
|
||||||
# version writes a different mf.pb.go, so the script refuses to run.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
||||||
|
|
||||||
# protoc 33.4 names itself v6.33.4 in the mf.pb.go header.
|
|
||||||
PROTOC_VERSION="33.4"
|
|
||||||
PROTOC_GEN_GO_VERSION="v1.36.11"
|
|
||||||
|
|
||||||
# require_version <command> <its exact --version output>
|
|
||||||
require_version() {
|
|
||||||
actual="$("$1" --version 2>/dev/null || true)"
|
|
||||||
if [ "$actual" != "$2" ]; then
|
|
||||||
echo "generate: needs $2 on PATH, found: ${actual:-none}" >&2
|
|
||||||
echo " README.md says how to install it." >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
# sha256 <file>: print "<hash> <file>", with sha256sum, or with shasum
|
|
||||||
# where there is no sha256sum.
|
|
||||||
sha256() {
|
|
||||||
if command -v sha256sum >/dev/null 2>&1; then
|
|
||||||
sha256sum "$1"
|
|
||||||
elif command -v shasum >/dev/null 2>&1; then
|
|
||||||
shasum -a 256 "$1"
|
|
||||||
else
|
|
||||||
echo "generate: needs sha256sum or shasum on PATH" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
main() {
|
|
||||||
cd "$ROOT/mfer"
|
|
||||||
require_version protoc "libprotoc $PROTOC_VERSION"
|
|
||||||
require_version protoc-gen-go "protoc-gen-go $PROTOC_GEN_GO_VERSION"
|
|
||||||
# Hashed before regenerating, so a missing hash tool stops the script
|
|
||||||
# before it changes anything. Regenerating leaves mf.proto as it is.
|
|
||||||
proto_hash="$(sha256 mf.proto)"
|
|
||||||
go generate .
|
|
||||||
echo "$proto_hash" >mf.proto.sha256
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
+11
@@ -4,8 +4,19 @@ set -eu
|
|||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
|
|
||||||
|
# Regenerate mfer/mf.pb.go from mfer/mf.proto if it is missing or stale
|
||||||
|
# (mirrors the old Makefile prerequisite; the generated file is
|
||||||
|
# committed, so this is normally a no-op).
|
||||||
|
ensure_pb() {
|
||||||
|
if [ ! -f mfer/mf.pb.go ] ||
|
||||||
|
[ -n "$(find mfer/mf.proto -newer mfer/mf.pb.go 2>/dev/null)" ]; then
|
||||||
|
(cd mfer && go generate .)
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
ensure_pb
|
||||||
go test -timeout 30s -race -cover ./... ||
|
go test -timeout 30s -race -cover ./... ||
|
||||||
{
|
{
|
||||||
echo "--- Rerunning with -v for details ---"
|
echo "--- Rerunning with -v for details ---"
|
||||||
|
|||||||
Reference in New Issue
Block a user