Porównaj commity
1
Commity
| Autor | SHA1 | Data | |
|---|---|---|---|
|
|
114ba0a29f |
+1
-1
@@ -8,7 +8,7 @@ vendor.tzst
|
|||||||
modcache.tzst
|
modcache.tzst
|
||||||
|
|
||||||
# Generated manifest files
|
# Generated manifest files
|
||||||
/index.mf
|
.index.mf
|
||||||
|
|
||||||
# Secrets
|
# Secrets
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
+1
-3
@@ -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`)
|
||||||
|
|
||||||
|
|||||||
+10
-5
@@ -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
|
||||||
|
|||||||
+4
-15
@@ -36,12 +36,12 @@ Generate a manifest for a directory tree, verify it later, and fetch a published
|
|||||||
tree by URL:
|
tree by URL:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
# Write index.mf, a manifest of the files under the current directory.
|
# Write .index.mf, a manifest of the files under the current directory.
|
||||||
bin/mfer gen .
|
bin/mfer gen .
|
||||||
|
|
||||||
# Verify the files on disk against the manifest. Exits nonzero if any file
|
# Verify the files on disk against the manifest. Exits nonzero if any file
|
||||||
# is missing or corrupted.
|
# is missing or corrupted.
|
||||||
bin/mfer check index.mf
|
bin/mfer check .index.mf
|
||||||
|
|
||||||
# Download and cryptographically verify a tree published over HTTP: mfer
|
# Download and cryptographically verify a tree published over HTTP: mfer
|
||||||
# fetches <url>/index.mf, then downloads every file it lists.
|
# fetches <url>/index.mf, then downloads every file it lists.
|
||||||
@@ -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
|
||||||
|
|||||||
+15
-6
@@ -3,33 +3,42 @@ module sneak.berlin/go/mfer
|
|||||||
go 1.23
|
go 1.23
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/apex/log v1.9.0
|
||||||
github.com/davecgh/go-spew v1.1.1
|
github.com/davecgh/go-spew v1.1.1
|
||||||
github.com/dustin/go-humanize v1.0.1
|
github.com/dustin/go-humanize v1.0.1
|
||||||
github.com/google/uuid v1.1.2
|
github.com/google/uuid v1.1.2
|
||||||
github.com/klauspost/compress v1.18.2
|
github.com/klauspost/compress v1.18.2
|
||||||
github.com/multiformats/go-multihash v0.2.3
|
github.com/multiformats/go-multihash v0.2.3
|
||||||
|
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
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211
|
|
||||||
google.golang.org/protobuf v1.28.1
|
google.golang.org/protobuf v1.28.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
github.com/atomicgo/cursor v0.0.1 // indirect
|
||||||
|
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
|
||||||
|
github.com/fatih/color v1.7.0 // 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
|
||||||
github.com/kr/pretty v0.2.0 // indirect
|
github.com/mattn/go-colorable v0.1.2 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.8 // indirect
|
||||||
|
github.com/mattn/go-runewidth v0.0.13 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.0 // indirect
|
github.com/minio/sha256-simd v1.0.0 // indirect
|
||||||
github.com/mr-tron/base58 v1.2.0 // indirect
|
github.com/mr-tron/base58 v1.2.0 // indirect
|
||||||
github.com/multiformats/go-varint v0.0.6 // indirect
|
github.com/multiformats/go-varint v0.0.6 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/rivo/uniseg v0.2.0 // indirect
|
||||||
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/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // 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/text v0.3.6 // indirect
|
golang.org/x/text v0.3.6 // indirect
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
lukechampine.com/blake3 v1.1.6 // indirect
|
lukechampine.com/blake3 v1.1.6 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
+83
-6
@@ -38,6 +38,21 @@ cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3f
|
|||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
|
||||||
|
github.com/MarvinJWendt/testza v0.1.0/go.mod h1:7AxNvlfeHP7Z/hDQ5JtE3OKYT3XFUeLCDE2DQninSqs=
|
||||||
|
github.com/MarvinJWendt/testza v0.2.1/go.mod h1:God7bhG8n6uQxwdScay+gjm9/LnO4D3kkcZX4hv9Rp8=
|
||||||
|
github.com/MarvinJWendt/testza v0.2.8/go.mod h1:nwIcjmr0Zz+Rcwfh3/4UhBp7ePKVhuBExvZqnKYWlII=
|
||||||
|
github.com/MarvinJWendt/testza v0.2.10/go.mod h1:pd+VWsoGUiFtq+hRKSU1Bktnn+DMCSrDrXDpX2bG66k=
|
||||||
|
github.com/MarvinJWendt/testza v0.2.12 h1:/PRp/BF+27t2ZxynTiqj0nyND5PbOtfJS0SuTuxmgeg=
|
||||||
|
github.com/MarvinJWendt/testza v0.2.12/go.mod h1:JOIegYyV7rX+7VZ9r77L/eH6CfJHHzXjB69adAhzZkI=
|
||||||
|
github.com/apex/log v1.9.0 h1:FHtw/xuaM8AgmvDDTI9fiwoAL25Sq2cxojnZICUU8l0=
|
||||||
|
github.com/apex/log v1.9.0/go.mod h1:m82fZlWIuiWzWP04XCTXmnX0xRkYYbCdYn8jbJeLBEA=
|
||||||
|
github.com/apex/logs v1.0.0/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo=
|
||||||
|
github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE=
|
||||||
|
github.com/aphistic/sweet v0.2.0/go.mod h1:fWDlIh/isSE9n6EPsRmC0det+whmX6dJid3stzu0Xys=
|
||||||
|
github.com/atomicgo/cursor v0.0.1 h1:xdogsqa6YYlLfM+GyClC/Lchf7aiMerFiZQn7soTOoU=
|
||||||
|
github.com/atomicgo/cursor v0.0.1/go.mod h1:cBON2QmmrysudxNBFthvMtN32r3jxVRIvzkUiF/RuIk=
|
||||||
|
github.com/aws/aws-sdk-go v1.20.6/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||||
|
github.com/aybabtme/rgbterm v0.0.0-20170906152045-cc83f3b3ce59/go.mod h1:q/89r3U2H7sSsE2t6Kca0lfwTK8JdoNGS/yzM/4iH5I=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||||
@@ -46,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=
|
||||||
@@ -59,9 +74,13 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m
|
|||||||
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
|
||||||
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
|
||||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||||
|
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||||
|
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||||
|
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||||
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||||
|
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||||
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
@@ -115,15 +134,21 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe
|
|||||||
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
||||||
|
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
|
||||||
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||||
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
|
||||||
|
github.com/gookit/color v1.4.2 h1:tXy44JFSFkKnELV6WaMo/lLfu/meqITX3iAV52do7lk=
|
||||||
|
github.com/gookit/color v1.4.2/go.mod h1:fqRyamkC1W8uxl+lxCQxOT09l/vYfZ+QeiX3rKQHCoQ=
|
||||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||||
|
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||||
|
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
|
||||||
|
github.com/jpillora/backoff v0.0.0-20180909062703-3050d21c67d7/go.mod h1:2iMrUgbbvHEiQClaW2NsSzMyGHqN+rDFqY705q49KG0=
|
||||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
@@ -133,12 +158,22 @@ github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa02
|
|||||||
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4=
|
||||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
|
||||||
|
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs=
|
||||||
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ=
|
||||||
|
github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU=
|
||||||
|
github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||||
|
github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE=
|
||||||
|
github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
|
||||||
|
github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU=
|
||||||
|
github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
|
github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE=
|
||||||
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g=
|
||||||
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM=
|
||||||
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
|
||||||
@@ -147,14 +182,32 @@ github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7B
|
|||||||
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
|
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
|
||||||
github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY=
|
github.com/multiformats/go-varint v0.0.6 h1:gk85QWKxh3TazbLxED/NlDVv8+q+ReFJk7Y2W/KhfNY=
|
||||||
github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
|
github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE=
|
||||||
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
|
github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||||
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||||
|
github.com/pterm/pterm v0.12.27/go.mod h1:PhQ89w4i95rhgE+xedAoqous6K9X+r6aSOI2eFF7DZI=
|
||||||
|
github.com/pterm/pterm v0.12.29/go.mod h1:WI3qxgvoQFFGKGjGnJR849gU0TsEOvKn5Q8LlY1U7lg=
|
||||||
|
github.com/pterm/pterm v0.12.30/go.mod h1:MOqLIyMOgmTDz9yorcYbcw+HsgoZo3BQfg2wtl3HEFE=
|
||||||
|
github.com/pterm/pterm v0.12.31/go.mod h1:32ZAWZVXD7ZfG0s8qqHXePte42kdz8ECtRyEejaWgXU=
|
||||||
|
github.com/pterm/pterm v0.12.33/go.mod h1:x+h2uL+n7CP/rel9+bImHD5lF3nM9vJj80k9ybiiTTE=
|
||||||
|
github.com/pterm/pterm v0.12.35 h1:A/vHwDM+WByn0sTPlpL2L6kOTy12xqZuwNFMF/NlA+U=
|
||||||
|
github.com/pterm/pterm v0.12.35/go.mod h1:NjiL09hFhT/vWjQHSj1athJpx6H8cjpHXNAK5bUw8T8=
|
||||||
|
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
|
github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
|
||||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||||
|
github.com/smartystreets/assertions v1.0.0/go.mod h1:kHHU4qYBaI3q23Pp3VPrmWhuIUrLW/7eUrw0BU5VaoM=
|
||||||
|
github.com/smartystreets/go-aws-auth v0.0.0-20180515143844-0c1422d1fdb9/go.mod h1:SnhjPscd9TpLiy1LpzGSKh3bXCfxxXuqd9xmQJy3slM=
|
||||||
|
github.com/smartystreets/gunit v1.0.0/go.mod h1:qwPWnhz6pn0NnRBP++URONOVyNkPyr4SauJk4cUOwJs=
|
||||||
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
|
||||||
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||||
github.com/spf13/afero v1.8.0 h1:5MmtuhAgYeU6qpa7w7bP0dv6MBYuup0vekhSpSkoq60=
|
github.com/spf13/afero v1.8.0 h1:5MmtuhAgYeU6qpa7w7bP0dv6MBYuup0vekhSpSkoq60=
|
||||||
@@ -162,17 +215,28 @@ github.com/spf13/afero v1.8.0/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfA
|
|||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
github.com/urfave/cli/v2 v2.27.7 h1:bH59vdhbjLv3LAvIu6gd0usJHgoTTPhCFib8qqOwXYU=
|
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
|
||||||
github.com/urfave/cli/v2 v2.27.7/go.mod h1:CyNAG/xg+iAOg0N4MPGZqVmv2rCoP267496AOXUZjA4=
|
github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4=
|
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
|
||||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
|
github.com/tj/go-buffer v1.1.0/go.mod h1:iyiJpfFcR2B9sXu7KvjbT9fpM4mOelRSDTbntVj52Uc=
|
||||||
|
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-spin v1.1.0/go.mod h1:Mg1mzmePZm4dva8Qz60H2lHwmJ2loum4VIrLgVnKwh4=
|
||||||
|
github.com/urfave/cli/v2 v2.23.6 h1:iWmtKD+prGo1nKUtLO0Wg4z9esfBM4rAV4QRLQiEmJ4=
|
||||||
|
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/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs=
|
||||||
|
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU=
|
||||||
|
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=
|
||||||
@@ -184,6 +248,7 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
|||||||
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
|
||||||
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
@@ -227,6 +292,7 @@ golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||||||
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
|
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
@@ -276,7 +342,9 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ
|
|||||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
@@ -307,11 +375,15 @@ golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U=
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
@@ -473,8 +545,13 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
|
|||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||||
|
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||||
|
|||||||
+19
-32
@@ -11,7 +11,6 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/dustin/go-humanize"
|
"github.com/dustin/go-humanize"
|
||||||
@@ -76,22 +75,25 @@ func safeRateUint64(rate float64) uint64 {
|
|||||||
return uint64(rate)
|
return uint64(rate)
|
||||||
}
|
}
|
||||||
|
|
||||||
// findManifest returns the path of the manifest with the default name in
|
// findManifest looks for a manifest file in the given directory.
|
||||||
// dir, or an error if there is none.
|
// It checks for index.mf and .index.mf, returning the first one found.
|
||||||
func findManifest(fs afero.Fs, dir string) (string, error) {
|
func findManifest(fs afero.Fs, dir string) (string, error) {
|
||||||
path := filepath.Join(dir, defaultManifestName)
|
candidates := []string{"index.mf", ".index.mf"}
|
||||||
|
for _, name := range candidates {
|
||||||
|
path := filepath.Join(dir, name)
|
||||||
|
|
||||||
exists, err := afero.Exists(fs, path)
|
exists, err := afero.Exists(fs, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists {
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !exists {
|
return "", fmt.Errorf(
|
||||||
return "", fmt.Errorf("%w in %s (looked for %s)",
|
"%w in %s (looked for index.mf and .index.mf)", errNoManifestFound, dir)
|
||||||
errNoManifestFound, dir, defaultManifestName)
|
|
||||||
}
|
|
||||||
|
|
||||||
return path, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// fetchManifestToTemp downloads a manifest URL to a temporary file and
|
// fetchManifestToTemp downloads a manifest URL to a temporary file and
|
||||||
@@ -164,9 +166,7 @@ func verifyRequiredSigner(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// reportCheckProgress renders progress updates until the channel closes.
|
// reportCheckProgress renders progress updates until the channel closes.
|
||||||
func reportCheckProgress(progress <-chan mfer.CheckStatus, wg *sync.WaitGroup) {
|
func reportCheckProgress(progress <-chan mfer.CheckStatus) {
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
for status := range progress {
|
for status := range progress {
|
||||||
if status.ETA > 0 {
|
if status.ETA > 0 {
|
||||||
log.Progressf("Checking: %d/%d files, %s/s, ETA %s, %d failures",
|
log.Progressf("Checking: %d/%d files, %s/s, ETA %s, %d failures",
|
||||||
@@ -238,17 +238,11 @@ func runCheck(ctx *cli.Context, chk *mfer.Checker, showProgress bool) (int64, er
|
|||||||
results := make(chan mfer.Result, 1)
|
results := make(chan mfer.Result, 1)
|
||||||
|
|
||||||
// Set up progress channel
|
// Set up progress channel
|
||||||
var (
|
var progress chan mfer.CheckStatus
|
||||||
progress chan mfer.CheckStatus
|
|
||||||
progressWg sync.WaitGroup
|
|
||||||
)
|
|
||||||
|
|
||||||
if showProgress {
|
if showProgress {
|
||||||
progress = make(chan mfer.CheckStatus, 1)
|
progress = make(chan mfer.CheckStatus, 1)
|
||||||
|
|
||||||
progressWg.Add(1)
|
go reportCheckProgress(progress)
|
||||||
|
|
||||||
go reportCheckProgress(progress, &progressWg)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process results in a goroutine
|
// Process results in a goroutine
|
||||||
@@ -260,9 +254,6 @@ func runCheck(ctx *cli.Context, chk *mfer.Checker, showProgress bool) (int64, er
|
|||||||
|
|
||||||
// Run check
|
// Run check
|
||||||
err := chk.Check(ctx.Context, results, progress)
|
err := chk.Check(ctx.Context, results, progress)
|
||||||
|
|
||||||
progressWg.Wait()
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, fmt.Errorf("check failed: %w", err)
|
return 0, fmt.Errorf("check failed: %w", err)
|
||||||
}
|
}
|
||||||
@@ -307,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)
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-282
@@ -8,12 +8,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/spf13/afero"
|
"github.com/spf13/afero"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -29,11 +25,11 @@ const (
|
|||||||
testFile1 = "/testdir/file1.txt"
|
testFile1 = "/testdir/file1.txt"
|
||||||
testMF = "/testdir/test.mf"
|
testMF = "/testdir/test.mf"
|
||||||
testOutput = "/output.mf"
|
testOutput = "/output.mf"
|
||||||
|
testOutputTmp = "/output.mf.tmp"
|
||||||
testManifest = "/manifest.mf"
|
testManifest = "/manifest.mf"
|
||||||
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")
|
||||||
@@ -46,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) {
|
||||||
@@ -251,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()
|
||||||
|
|
||||||
@@ -394,74 +294,6 @@ func TestGenerateAndCheckCommand(t *testing.T) {
|
|||||||
assert.Equal(t, 0, exitCode, "check failed: %s", testStderr(t, opts))
|
assert.Equal(t, 0, exitCode, "check failed: %s", testStderr(t, opts))
|
||||||
}
|
}
|
||||||
|
|
||||||
// sharedWriter appends to a buffer shared with other sharedWriters, so
|
|
||||||
// output written to stdout and stderr is kept in the order it was written.
|
|
||||||
// Each write first waits for delay.
|
|
||||||
type sharedWriter struct {
|
|
||||||
mu *sync.Mutex
|
|
||||||
buf *bytes.Buffer
|
|
||||||
delay time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w sharedWriter) Write(p []byte) (int, error) {
|
|
||||||
time.Sleep(w.delay)
|
|
||||||
|
|
||||||
w.mu.Lock()
|
|
||||||
defer w.mu.Unlock()
|
|
||||||
|
|
||||||
return w.buf.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestCheckClearsProgressBeforeSummary asserts that check --progress writes
|
|
||||||
// its last progress line and clears it before it logs the summary. Progress
|
|
||||||
// writes are slowed down, so a progress goroutine that check did not wait for
|
|
||||||
// would write after the summary, or after the run has returned.
|
|
||||||
//
|
|
||||||
// Progress lines and log lines take the logger's lock, and when the progress
|
|
||||||
// goroutine gets it first the clear lands before the summary even without the
|
|
||||||
// wait. Which goroutine gets it first changes from run to run, so check runs
|
|
||||||
// ten times.
|
|
||||||
func TestCheckClearsProgressBeforeSummary(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
fs := afero.NewMemMapFs()
|
|
||||||
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
|
||||||
writeTestFile(t, fs, testFile1, "hello world")
|
|
||||||
|
|
||||||
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", testMF, testDir}, fs)
|
|
||||||
require.Equal(t, 0, runCLI(opts), "generate failed: %s", testStderr(t, opts))
|
|
||||||
|
|
||||||
for range 10 {
|
|
||||||
var (
|
|
||||||
mu sync.Mutex
|
|
||||||
output bytes.Buffer
|
|
||||||
)
|
|
||||||
|
|
||||||
opts = testOpts([]string{
|
|
||||||
testApp, cmdCheck, "--progress", testFlagBase, testDir, testMF,
|
|
||||||
}, fs)
|
|
||||||
opts.Stdout = sharedWriter{mu: &mu, buf: &output, delay: 100 * time.Millisecond}
|
|
||||||
opts.Stderr = sharedWriter{mu: &mu, buf: &output}
|
|
||||||
require.Equal(t, 0, runCLI(opts))
|
|
||||||
|
|
||||||
mu.Lock()
|
|
||||||
got := output.String()
|
|
||||||
mu.Unlock()
|
|
||||||
|
|
||||||
lastProgress := strings.Index(got, "Checking: 1/1 files")
|
|
||||||
progressDone := strings.Index(got, "\r\033[K")
|
|
||||||
summary := strings.Index(got, "checked 1 files")
|
|
||||||
|
|
||||||
require.NotEqual(t, -1, lastProgress, "no last progress line in %q", got)
|
|
||||||
require.NotEqual(t, -1, progressDone, "progress line never cleared in %q", got)
|
|
||||||
require.NotEqual(t, -1, summary, "no summary in %q", got)
|
|
||||||
require.Less(t, lastProgress, progressDone,
|
|
||||||
"progress cleared before its last line in %q", got)
|
|
||||||
require.Less(t, progressDone, summary,
|
|
||||||
"summary logged before progress was cleared in %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCheckCommandWithMissingFile(t *testing.T) {
|
func TestCheckCommandWithMissingFile(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -573,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())
|
||||||
@@ -600,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)
|
||||||
}
|
}
|
||||||
@@ -751,7 +577,7 @@ func TestGenerateAtomicWriteNoTempFileOnSuccess(t *testing.T) {
|
|||||||
assert.True(t, exists, "output file should exist")
|
assert.True(t, exists, "output file should exist")
|
||||||
|
|
||||||
// Verify temp file does NOT exist
|
// Verify temp file does NOT exist
|
||||||
tmpExists, err := afero.Exists(fs, manifestTempPath(testOutput))
|
tmpExists, err := afero.Exists(fs, testOutputTmp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, tmpExists,
|
assert.False(t, tmpExists,
|
||||||
"temp file should not exist after successful generation")
|
"temp file should not exist after successful generation")
|
||||||
@@ -783,7 +609,7 @@ func TestGenerateAtomicWriteOverwriteWithForce(t *testing.T) {
|
|||||||
"manifest should be overwritten")
|
"manifest should be overwritten")
|
||||||
|
|
||||||
// Verify temp file does NOT exist
|
// Verify temp file does NOT exist
|
||||||
tmpExists, err := afero.Exists(fs, manifestTempPath(testOutput))
|
tmpExists, err := afero.Exists(fs, testOutputTmp)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.False(t, tmpExists,
|
assert.False(t, tmpExists,
|
||||||
"temp file should not exist after successful generation")
|
"temp file should not exist after successful generation")
|
||||||
@@ -812,102 +638,6 @@ func TestGenerateFailsWithoutForceWhenOutputExists(t *testing.T) {
|
|||||||
assert.Equal(t, "existing", string(content), "original file should be preserved")
|
assert.Equal(t, "existing", string(content), "original file should be preserved")
|
||||||
}
|
}
|
||||||
|
|
||||||
// manifestPaths returns the file paths listed by the manifest at path.
|
|
||||||
func manifestPaths(t *testing.T, fs afero.Fs, path string) []string {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
manifest, err := mfer.NewManifestFromFile(&mfer.ManifestFromFileOptions{
|
|
||||||
Path: path,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
paths := make([]string, 0, len(manifest.Files()))
|
|
||||||
for _, f := range manifest.Files() {
|
|
||||||
paths = append(paths, f.GetPath())
|
|
||||||
}
|
|
||||||
|
|
||||||
return paths
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGenerateLeavesOutputOutOfListing overwrites an output file inside the
|
|
||||||
// scanned tree with --force: the old file is not listed, even when the tree
|
|
||||||
// is named through a symlink, while a file named index.mf in a subdirectory
|
|
||||||
// still is. The output file is recognized by file identity, which needs
|
|
||||||
// the real filesystem.
|
|
||||||
func TestGenerateLeavesOutputOutOfListing(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// Paths are relative to a temp dir holding data/tree and link, a
|
|
||||||
// symlink to data.
|
|
||||||
for name, tc := range map[string]struct{ input, output string }{
|
|
||||||
"default name": {"data/tree", "data/tree/index.mf"},
|
|
||||||
"other name in a subdirectory": {"data/tree", "data/tree/sub/listing.mf"},
|
|
||||||
"tree named through a symlink": {"link/tree", "data/tree/index.mf"},
|
|
||||||
} {
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
root := t.TempDir()
|
|
||||||
tree := filepath.Join(root, "data", "tree")
|
|
||||||
output := filepath.Join(root, tc.output)
|
|
||||||
|
|
||||||
fs := afero.NewOsFs()
|
|
||||||
require.NoError(t, fs.MkdirAll(filepath.Join(tree, "sub"), 0o750))
|
|
||||||
require.NoError(t,
|
|
||||||
os.Symlink(filepath.Join(root, "data"), filepath.Join(root, "link")))
|
|
||||||
writeTestFile(t, fs, filepath.Join(tree, testFileTxt), "hello")
|
|
||||||
writeTestFile(t, fs, filepath.Join(tree, "sub", "index.mf"), "an ordinary file")
|
|
||||||
writeTestFile(t, fs, output, "previous manifest")
|
|
||||||
|
|
||||||
opts := testOpts([]string{
|
|
||||||
testApp, cmdGenerate, "-q", "--force", "-o", output, filepath.Join(root, tc.input),
|
|
||||||
}, fs)
|
|
||||||
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
|
|
||||||
|
|
||||||
assert.ElementsMatch(t, []string{testFileTxt, "sub/index.mf"},
|
|
||||||
manifestPaths(t, fs, output))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGenerateDefaultOutputLeftOutOfListing runs gen with --force and no
|
|
||||||
// other arguments, so it scans the current directory and writes the
|
|
||||||
// relative path index.mf: the index.mf already there is not listed.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest // changes the process-global working directory
|
|
||||||
func TestGenerateDefaultOutputLeftOutOfListing(t *testing.T) {
|
|
||||||
chdirTemp(t)
|
|
||||||
|
|
||||||
fs := afero.NewOsFs()
|
|
||||||
writeTestFile(t, fs, testFileTxt, "hello")
|
|
||||||
writeTestFile(t, fs, "index.mf", "previous manifest")
|
|
||||||
|
|
||||||
opts := testOpts([]string{testApp, cmdGenerate, "-q", "--force"}, fs)
|
|
||||||
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
|
|
||||||
|
|
||||||
assert.Equal(t, []string{testFileTxt}, manifestPaths(t, fs, "index.mf"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGenerateLeavesLeftoverTempFileOutOfListing runs gen where an
|
|
||||||
// interrupted run left its temp file beside the output: the leftover is
|
|
||||||
// not listed, and gen does not fail when it overwrites it.
|
|
||||||
func TestGenerateLeavesLeftoverTempFileOutOfListing(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
root := t.TempDir()
|
|
||||||
output := filepath.Join(root, "index.mf")
|
|
||||||
|
|
||||||
fs := afero.NewOsFs()
|
|
||||||
writeTestFile(t, fs, filepath.Join(root, testFileTxt), "hello")
|
|
||||||
writeTestFile(t, fs, manifestTempPath(output), "part of a manifest")
|
|
||||||
|
|
||||||
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", output, root}, fs)
|
|
||||||
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
|
|
||||||
|
|
||||||
assert.Equal(t, []string{testFileTxt}, manifestPaths(t, fs, output))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGenerateAtomicWriteUsesTemp(t *testing.T) {
|
func TestGenerateAtomicWriteUsesTemp(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -930,7 +660,7 @@ func TestGenerateAtomicWriteUsesTemp(t *testing.T) {
|
|||||||
exists, _ := afero.Exists(fs, testOutput)
|
exists, _ := afero.Exists(fs, testOutput)
|
||||||
assert.True(t, exists, "output file should exist")
|
assert.True(t, exists, "output file should exist")
|
||||||
|
|
||||||
tmpExists, _ := afero.Exists(fs, manifestTempPath(testOutput))
|
tmpExists, _ := afero.Exists(fs, testOutputTmp)
|
||||||
assert.False(t, tmpExists, "temp file should be cleaned up")
|
assert.False(t, tmpExists, "temp file should be cleaned up")
|
||||||
|
|
||||||
// Verify manifest is valid (not empty)
|
// Verify manifest is valid (not empty)
|
||||||
@@ -996,7 +726,7 @@ func TestGenerateAtomicWriteCleansUpOnError(t *testing.T) {
|
|||||||
"output file should not exist after failed generation (atomic write)")
|
"output file should not exist after failed generation (atomic write)")
|
||||||
|
|
||||||
// Temp file should also not exist
|
// Temp file should also not exist
|
||||||
tmpExists, _ := afero.Exists(baseFs, manifestTempPath(testOutput))
|
tmpExists, _ := afero.Exists(baseFs, testOutputTmp)
|
||||||
assert.False(t, tmpExists,
|
assert.False(t, tmpExists,
|
||||||
"temp file should be cleaned up after failed generation")
|
"temp file should be cleaned up after failed generation")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|
||||||
@@ -78,7 +74,7 @@ func TestNoManifestFoundMessage(t *testing.T) {
|
|||||||
_, err := findManifest(afero.NewMemMapFs(), "/tmp/x")
|
_, err := findManifest(afero.NewMemMapFs(), "/tmp/x")
|
||||||
require.ErrorIs(t, err, errNoManifestFound)
|
require.ErrorIs(t, err, errNoManifestFound)
|
||||||
assert.EqualError(t, err,
|
assert.EqualError(t, err,
|
||||||
"no manifest found in /tmp/x (looked for index.mf)")
|
"no manifest found in /tmp/x (looked for index.mf and .index.mf)")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVerifyRequiredSignerMessages(t *testing.T) {
|
func TestVerifyRequiredSignerMessages(t *testing.T) {
|
||||||
@@ -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())
|
||||||
|
|
||||||
@@ -272,15 +264,10 @@ func TestFetchManifestHTTPStatusMessage(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
mfa := &CLIApp{Fs: afero.NewMemMapFs()}
|
set := flag.NewFlagSet("fetch", flag.ContinueOnError)
|
||||||
|
|
||||||
set := flag.NewFlagSet(cmdFetch, flag.ContinueOnError)
|
|
||||||
for _, f := range mfa.fetchCommand().Flags {
|
|
||||||
require.NoError(t, f.Apply(set))
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, set.Parse([]string{server.URL}))
|
require.NoError(t, set.Parse([]string{server.URL}))
|
||||||
|
|
||||||
|
mfa := &CLIApp{Fs: afero.NewMemMapFs()}
|
||||||
ctx := urfcli.NewContext(nil, set, nil)
|
ctx := urfcli.NewContext(nil, set, nil)
|
||||||
|
|
||||||
// fetchManifestOperation logs to the process-global logger.
|
// fetchManifestOperation logs to the process-global logger.
|
||||||
@@ -298,11 +285,8 @@ func TestFetchFileHTTPStatusMessage(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
// downloadFile logs each retry of the 500 to the process-global logger.
|
err := downloadFile(context.Background(), server.URL+"/x", "x",
|
||||||
err := runLocked(func() error {
|
&mfer.MFFilePath{}, nil)
|
||||||
return downloadFile(context.Background(), testClient(), server.URL+"/x", "x",
|
|
||||||
&mfer.MFFilePath{}, nil)
|
|
||||||
})
|
|
||||||
require.ErrorIs(t, err, errHTTPStatus)
|
require.ErrorIs(t, err, errHTTPStatus)
|
||||||
assert.EqualError(t, err, "HTTP 500")
|
assert.EqualError(t, err, "HTTP 500")
|
||||||
}
|
}
|
||||||
|
|||||||
+58
-173
@@ -7,13 +7,11 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math/rand/v2"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -47,33 +45,12 @@ const (
|
|||||||
bpsPerGbps = 1e9
|
bpsPerGbps = 1e9
|
||||||
bpsPerMbps = 1e6
|
bpsPerMbps = 1e6
|
||||||
bpsPerKbps = 1e3
|
bpsPerKbps = 1e3
|
||||||
|
|
||||||
// httpTimeout is the default time limit for one HTTP request, from
|
|
||||||
// connecting to reading the last byte of the body. It also bounds how
|
|
||||||
// long one file may take to download; fetch's --timeout changes it.
|
|
||||||
httpTimeout = 10 * time.Minute
|
|
||||||
|
|
||||||
// fetchAttempts is how many times fetch tries a request before it
|
|
||||||
// gives up on a transient failure.
|
|
||||||
fetchAttempts = 5
|
|
||||||
|
|
||||||
// firstRetryDelay is the longest fetch waits before its first retry.
|
|
||||||
// The limit doubles for each retry after that; the wait itself is
|
|
||||||
// random up to the limit.
|
|
||||||
firstRetryDelay = time.Second
|
|
||||||
|
|
||||||
// maxRetryAfter is the longest wait a server's Retry-After header can
|
|
||||||
// ask for. A server asking for longer fails the request at once.
|
|
||||||
maxRetryAfter = time.Minute
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// errURLRequired indicates the fetch command was run without a URL
|
// errURLRequired indicates the fetch command was run without a URL
|
||||||
// argument.
|
// argument.
|
||||||
errURLRequired = errors.New("URL argument required")
|
errURLRequired = errors.New("URL argument required")
|
||||||
// errInvalidTimeout indicates a fetch --timeout of zero or less, which
|
|
||||||
// http.Client would take as no time limit at all.
|
|
||||||
errInvalidTimeout = errors.New("--timeout must be greater than zero")
|
|
||||||
// errEmptyPath indicates an empty file path in the manifest.
|
// errEmptyPath indicates an empty file path in the manifest.
|
||||||
errEmptyPath = errors.New("empty path")
|
errEmptyPath = errors.New("empty path")
|
||||||
// errAbsolutePath indicates an absolute file path in the manifest.
|
// errAbsolutePath indicates an absolute file path in the manifest.
|
||||||
@@ -101,109 +78,24 @@ type DownloadProgress struct {
|
|||||||
ETA time.Duration // Estimated time to completion
|
ETA time.Duration // Estimated time to completion
|
||||||
}
|
}
|
||||||
|
|
||||||
// retryingClient is the HTTP client fetch makes every request with. It
|
// httpGet issues a GET request for the given URL using the provided
|
||||||
// waits up to firstDelay, which must be positive, before its first retry
|
// context and returns the response. The caller must close the body.
|
||||||
// of a transient failure. Tests shorten both the client's timeout and
|
|
||||||
// firstDelay.
|
|
||||||
type retryingClient struct {
|
|
||||||
client *http.Client
|
|
||||||
firstDelay time.Duration
|
|
||||||
}
|
|
||||||
|
|
||||||
// get issues a GET for rawURL and passes a 200 OK response to use.
|
|
||||||
//
|
//
|
||||||
// A connection error, a timeout, or a 5xx or 429 status is retried, up to
|
// Errors are returned unwrapped: this helper replaced direct http.Get
|
||||||
// fetchAttempts tries in all. Before each retry it waits a random time up
|
// calls, and each caller already supplies its own context string, so
|
||||||
// to a limit that doubles each time, unless the server's Retry-After
|
// adding one here would change user-visible messages.
|
||||||
// header says how long to wait. Any other failure, an error from use
|
func httpGet(ctx context.Context, fileURL string) (*http.Response, error) {
|
||||||
// included, is returned at once and unwrapped; a non-OK status is
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileURL, nil)
|
||||||
// returned as errHTTPStatus. use must start over each time it is called,
|
|
||||||
// since a retry calls it again with a new response.
|
|
||||||
func (c retryingClient) get(
|
|
||||||
ctx context.Context, rawURL string, use func(*http.Response) error,
|
|
||||||
) error {
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
delay := c.firstDelay
|
resp, err := http.DefaultClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
for attempt := 1; ; attempt++ {
|
return nil, err
|
||||||
// Wait a random time up to delay, so that clients that failed
|
|
||||||
// together do not all retry together.
|
|
||||||
wait := rand.N(delay) //nolint:gosec // G404: jitter, not a secret
|
|
||||||
|
|
||||||
var retry bool
|
|
||||||
|
|
||||||
resp, err := c.client.Do(req)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case err != nil:
|
|
||||||
retry = isConnectionError(err)
|
|
||||||
case resp.StatusCode == http.StatusOK:
|
|
||||||
err = use(resp)
|
|
||||||
retry = isConnectionError(err)
|
|
||||||
|
|
||||||
_ = resp.Body.Close()
|
|
||||||
default:
|
|
||||||
_ = resp.Body.Close()
|
|
||||||
|
|
||||||
err = fmt.Errorf("%w %d", errHTTPStatus, resp.StatusCode)
|
|
||||||
retry = resp.StatusCode >= http.StatusInternalServerError ||
|
|
||||||
resp.StatusCode == http.StatusTooManyRequests
|
|
||||||
|
|
||||||
after, ok := retryAfter(resp.Header.Get("Retry-After"))
|
|
||||||
if ok {
|
|
||||||
wait = after
|
|
||||||
retry = retry && after <= maxRetryAfter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !retry || attempt == fetchAttempts || ctx.Err() != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Warnf("%s: %s, retrying in %s", rawURL, err, wait.Round(time.Millisecond))
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return ctx.Err()
|
|
||||||
case <-time.After(wait):
|
|
||||||
}
|
|
||||||
|
|
||||||
delay *= 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// isConnectionError reports whether err is a connection error or a
|
|
||||||
// timeout: a connection that could not be made, was reset, or closed
|
|
||||||
// early, or a request that ran out of time.
|
|
||||||
func isConnectionError(err error) bool {
|
|
||||||
var (
|
|
||||||
opErr *net.OpError
|
|
||||||
netErr net.Error
|
|
||||||
)
|
|
||||||
|
|
||||||
return errors.As(err, &opErr) ||
|
|
||||||
(errors.As(err, &netErr) && netErr.Timeout()) ||
|
|
||||||
errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF)
|
|
||||||
}
|
|
||||||
|
|
||||||
// retryAfter returns the wait a Retry-After header value asks for, given
|
|
||||||
// either in seconds or as an HTTP date. ok is false if there is none.
|
|
||||||
func retryAfter(value string) (time.Duration, bool) {
|
|
||||||
seconds, err := strconv.Atoi(value)
|
|
||||||
if err == nil {
|
|
||||||
return time.Duration(seconds) * time.Second, true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
when, err := http.ParseTime(value)
|
return resp, nil
|
||||||
if err == nil {
|
|
||||||
return time.Until(when), true
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0, false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// reportDownloadProgress renders download progress until the channel
|
// reportDownloadProgress renders download progress until the channel
|
||||||
@@ -227,23 +119,25 @@ func reportDownloadProgress(progress <-chan DownloadProgress, done chan<- struct
|
|||||||
}
|
}
|
||||||
|
|
||||||
// manifestBaseURL returns the URL of the directory containing the
|
// manifestBaseURL returns the URL of the directory containing the
|
||||||
// manifest.
|
// manifest, with a trailing slash.
|
||||||
func manifestBaseURL(manifestURL string) (*url.URL, error) {
|
func manifestBaseURL(manifestURL string) (*url.URL, error) {
|
||||||
parsed, err := url.Parse(manifestURL)
|
baseURL, err := url.Parse(manifestURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("fetch: invalid manifest URL: %w", err)
|
return nil, fmt.Errorf("fetch: invalid manifest URL: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JoinPath cleans the path it builds, so ".." drops the manifest's
|
baseURL.Path = path.Dir(baseURL.Path)
|
||||||
// file name.
|
if !strings.HasSuffix(baseURL.Path, "/") {
|
||||||
return parsed.JoinPath(".."), nil
|
baseURL.Path += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
return baseURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// downloadManifestFiles downloads every file in the manifest, reporting
|
// downloadManifestFiles downloads every file in the manifest, reporting
|
||||||
// progress on the progress channel.
|
// progress on the progress channel.
|
||||||
func downloadManifestFiles(
|
func downloadManifestFiles(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
client retryingClient,
|
|
||||||
baseURL *url.URL,
|
baseURL *url.URL,
|
||||||
files []*mfer.MFFilePath,
|
files []*mfer.MFFilePath,
|
||||||
progress chan<- DownloadProgress,
|
progress chan<- DownloadProgress,
|
||||||
@@ -255,12 +149,10 @@ func downloadManifestFiles(
|
|||||||
return fmt.Errorf("invalid path in manifest: %w", err)
|
return fmt.Errorf("invalid path in manifest: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// JoinPath takes escaped path text, so a name such as "100%.txt"
|
fileURL := baseURL.String() + encodeFilePath(f.GetPath())
|
||||||
// must be escaped first.
|
|
||||||
fileURL := baseURL.JoinPath(encodeFilePath(f.GetPath())).String()
|
|
||||||
log.Infof("fetching %s", f.GetPath())
|
log.Infof("fetching %s", f.GetPath())
|
||||||
|
|
||||||
err = downloadFile(ctx, client, fileURL, localPath, f, progress)
|
err = downloadFile(ctx, fileURL, localPath, f, progress)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to download %s: %w", f.GetPath(), err)
|
return fmt.Errorf("failed to download %s: %w", f.GetPath(), err)
|
||||||
}
|
}
|
||||||
@@ -276,41 +168,30 @@ func (mfa *CLIApp) fetchManifestOperation(ctx *cli.Context) error {
|
|||||||
return errURLRequired
|
return errURLRequired
|
||||||
}
|
}
|
||||||
|
|
||||||
timeout := ctx.Duration(flagTimeout)
|
inputURL := ctx.Args().Get(0)
|
||||||
if timeout <= 0 {
|
|
||||||
return errInvalidTimeout
|
|
||||||
}
|
|
||||||
|
|
||||||
manifestURL, err := resolveManifestURL(ctx.Args().Get(0))
|
manifestURL, err := resolveManifestURL(inputURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid URL: %w", err)
|
return fmt.Errorf("invalid URL: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
client := retryingClient{
|
|
||||||
client: &http.Client{Timeout: timeout},
|
|
||||||
firstDelay: firstRetryDelay,
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("fetching manifest from %s", manifestURL)
|
log.Infof("fetching manifest from %s", manifestURL)
|
||||||
|
|
||||||
// Read the whole manifest before parsing it, so that a connection
|
// Fetch manifest
|
||||||
// lost partway through is retried rather than reported as a bad
|
resp, err := httpGet(ctx.Context, manifestURL)
|
||||||
// manifest.
|
|
||||||
var manifestData []byte
|
|
||||||
|
|
||||||
err = client.get(ctx.Context, manifestURL, func(resp *http.Response) error {
|
|
||||||
var readErr error
|
|
||||||
|
|
||||||
manifestData, readErr = io.ReadAll(resp.Body)
|
|
||||||
|
|
||||||
return readErr
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to fetch manifest: %w", err)
|
return fmt.Errorf("failed to fetch manifest: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("failed to fetch manifest: %w %d",
|
||||||
|
errHTTPStatus, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
// Parse manifest
|
// Parse manifest
|
||||||
manifest, err := mfer.NewManifestFromReader(bytes.NewReader(manifestData))
|
manifest, err := mfer.NewManifestFromReader(resp.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse manifest: %w", err)
|
return fmt.Errorf("failed to parse manifest: %w", err)
|
||||||
}
|
}
|
||||||
@@ -340,7 +221,7 @@ func (mfa *CLIApp) fetchManifestOperation(ctx *cli.Context) error {
|
|||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
|
||||||
// Download each file
|
// Download each file
|
||||||
dlErr := downloadManifestFiles(ctx.Context, client, baseURL, files, progress)
|
dlErr := downloadManifestFiles(ctx.Context, baseURL, files, progress)
|
||||||
|
|
||||||
close(progress)
|
close(progress)
|
||||||
<-done
|
<-done
|
||||||
@@ -432,7 +313,7 @@ func checkNoSymlinks(p string) error {
|
|||||||
|
|
||||||
// resolveManifestURL takes a URL and returns the manifest URL.
|
// resolveManifestURL takes a URL and returns the manifest URL.
|
||||||
// If the URL already ends with .mf, it's returned as-is.
|
// If the URL already ends with .mf, it's returned as-is.
|
||||||
// Otherwise, the default manifest name is appended.
|
// Otherwise, index.mf is appended.
|
||||||
func resolveManifestURL(inputURL string) (string, error) {
|
func resolveManifestURL(inputURL string) (string, error) {
|
||||||
parsed, err := url.Parse(inputURL)
|
parsed, err := url.Parse(inputURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -444,7 +325,15 @@ func resolveManifestURL(inputURL string) (string, error) {
|
|||||||
return inputURL, nil
|
return inputURL, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsed.JoinPath(defaultManifestName).String(), nil
|
// Ensure path ends with /
|
||||||
|
if !strings.HasSuffix(parsed.Path, "/") {
|
||||||
|
parsed.Path += "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append index.mf
|
||||||
|
parsed.Path += "index.mf"
|
||||||
|
|
||||||
|
return parsed.String(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// progressWriter wraps an io.Writer and reports progress to a channel.
|
// progressWriter wraps an io.Writer and reports progress to a channel.
|
||||||
@@ -553,7 +442,6 @@ func verifyDownloadedHash(digest []byte, entry *mfer.MFFilePath) error {
|
|||||||
// Progress is reported via the progress channel.
|
// Progress is reported via the progress channel.
|
||||||
func downloadFile(
|
func downloadFile(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
client retryingClient,
|
|
||||||
fileURL, localPath string,
|
fileURL, localPath string,
|
||||||
entry *mfer.MFFilePath,
|
entry *mfer.MFFilePath,
|
||||||
progress chan<- DownloadProgress,
|
progress chan<- DownloadProgress,
|
||||||
@@ -581,21 +469,18 @@ func downloadFile(
|
|||||||
|
|
||||||
tmpPath := tempPathFor(localPath)
|
tmpPath := tempPathFor(localPath)
|
||||||
|
|
||||||
return client.get(ctx, fileURL, func(resp *http.Response) error {
|
// Fetch file
|
||||||
return saveResponse(resp, tmpPath, localPath, entry, progress)
|
resp, err := httpGet(ctx, fileURL)
|
||||||
})
|
if err != nil {
|
||||||
}
|
return fmt.Errorf("HTTP request failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = resp.Body.Close() }()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return fmt.Errorf("%w %d", errHTTPStatus, resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
// saveResponse writes resp's body to tmpPath, verifies it against entry,
|
|
||||||
// and renames it to localPath. It starts a new temp file each time and
|
|
||||||
// removes it on failure, so a retry after a failed try never appends to
|
|
||||||
// or keeps a partial file.
|
|
||||||
func saveResponse(
|
|
||||||
resp *http.Response,
|
|
||||||
tmpPath, localPath string,
|
|
||||||
entry *mfer.MFFilePath,
|
|
||||||
progress chan<- DownloadProgress,
|
|
||||||
) error {
|
|
||||||
// Determine expected size
|
// Determine expected size
|
||||||
expectedSize := entry.GetSize()
|
expectedSize := entry.GetSize()
|
||||||
|
|
||||||
@@ -604,7 +489,7 @@ func saveResponse(
|
|||||||
totalBytes = expectedSize
|
totalBytes = expectedSize
|
||||||
}
|
}
|
||||||
|
|
||||||
err := checkNoSymlinks(tmpPath)
|
err = checkNoSymlinks(tmpPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,24 +4,16 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"flag"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
|
||||||
"sync"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
|
||||||
|
|
||||||
"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"
|
||||||
urfcli "github.com/urfave/cli/v2"
|
|
||||||
"sneak.berlin/go/mfer/mfer"
|
"sneak.berlin/go/mfer/mfer"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -222,41 +214,6 @@ func fetchTestHandler(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// manifestOf scans a tree holding files and returns its manifest bytes.
|
|
||||||
func manifestOf(t *testing.T, files map[string][]byte) []byte {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
sourceFs := afero.NewMemMapFs()
|
|
||||||
for p, content := range files {
|
|
||||||
require.NoError(t, sourceFs.MkdirAll(filepath.Dir("/"+p), 0o755))
|
|
||||||
require.NoError(t, afero.WriteFile(sourceFs, "/"+p, content, 0o644))
|
|
||||||
}
|
|
||||||
|
|
||||||
return scanToManifest(t, sourceFs)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testClient returns the client tests download with. Its retries wait
|
|
||||||
// milliseconds rather than seconds.
|
|
||||||
func testClient() retryingClient {
|
|
||||||
return retryingClient{
|
|
||||||
client: &http.Client{Timeout: 10 * time.Second},
|
|
||||||
firstDelay: time.Millisecond,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// getNothing calls client.get for rawURL with a use that reads nothing,
|
|
||||||
// under a context that expires after timeout. It holds runMu, since get
|
|
||||||
// logs each retry to the process-global logger, and starts the timeout
|
|
||||||
// only once it has the lock.
|
|
||||||
func getNothing(client retryingClient, rawURL string, timeout time.Duration) error {
|
|
||||||
return runLocked(func() error {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
return client.get(ctx, rawURL, func(*http.Response) error { return nil })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
//nolint:paralleltest // changes the process-global working directory
|
//nolint:paralleltest // changes the process-global working directory
|
||||||
func TestFetchFromHTTP(t *testing.T) {
|
func TestFetchFromHTTP(t *testing.T) {
|
||||||
// Create source filesystem with test files
|
// Create source filesystem with test files
|
||||||
@@ -309,8 +266,7 @@ func TestFetchFromHTTP(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
fileURL := baseURL + f.GetPath()
|
fileURL := baseURL + f.GetPath()
|
||||||
err = downloadFile(context.Background(), testClient(),
|
err = downloadFile(context.Background(), fileURL, localPath, f, progress)
|
||||||
fileURL, localPath, f, progress)
|
|
||||||
require.NoError(t, err, "failed to download %s", f.GetPath())
|
require.NoError(t, err, "failed to download %s", f.GetPath())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -357,7 +313,7 @@ func TestFetchHashMismatch(t *testing.T) {
|
|||||||
chdirTemp(t)
|
chdirTemp(t)
|
||||||
|
|
||||||
// Try to download - should fail with hash mismatch
|
// Try to download - should fail with hash mismatch
|
||||||
err = downloadFile(context.Background(), testClient(),
|
err = downloadFile(context.Background(),
|
||||||
server.URL+"/file.txt", testFileTxt, files[0], nil)
|
server.URL+"/file.txt", testFileTxt, files[0], nil)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "mismatch")
|
assert.Contains(t, err.Error(), "mismatch")
|
||||||
@@ -403,7 +359,7 @@ func TestFetchSizeMismatch(t *testing.T) {
|
|||||||
chdirTemp(t)
|
chdirTemp(t)
|
||||||
|
|
||||||
// Try to download - should fail with size mismatch
|
// Try to download - should fail with size mismatch
|
||||||
err = downloadFile(context.Background(), testClient(),
|
err = downloadFile(context.Background(),
|
||||||
server.URL+"/file.txt", testFileTxt, files[0], nil)
|
server.URL+"/file.txt", testFileTxt, files[0], nil)
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
assert.Contains(t, err.Error(), "size mismatch")
|
assert.Contains(t, err.Error(), "size mismatch")
|
||||||
@@ -461,7 +417,7 @@ func TestFetchProgress(t *testing.T) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Download
|
// Download
|
||||||
err = downloadFile(context.Background(), testClient(),
|
err = downloadFile(context.Background(),
|
||||||
server.URL+"/large.txt", "large.txt", files[0], progress)
|
server.URL+"/large.txt", "large.txt", files[0], progress)
|
||||||
close(progress)
|
close(progress)
|
||||||
<-done
|
<-done
|
||||||
@@ -567,343 +523,3 @@ func TestFetchReplacesHardLinkAtTempName(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, "outside", string(outside), "fetch wrote outside the destination")
|
assert.Equal(t, "outside", string(outside), "fetch wrote outside the destination")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestGetRetriesTransientStatusesOnly answers every request with one
|
|
||||||
// status and counts the requests: a 5xx or 429 is retried until
|
|
||||||
// fetchAttempts runs out, and any other status fails at once.
|
|
||||||
func TestGetRetriesTransientStatusesOnly(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
status int
|
|
||||||
requests int32
|
|
||||||
}{
|
|
||||||
{http.StatusNotFound, 1},
|
|
||||||
{http.StatusForbidden, 1},
|
|
||||||
{http.StatusInternalServerError, fetchAttempts},
|
|
||||||
{http.StatusServiceUnavailable, fetchAttempts},
|
|
||||||
{http.StatusTooManyRequests, fetchAttempts},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(http.StatusText(tt.status), func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var requests atomic.Int32
|
|
||||||
|
|
||||||
server := httptest.NewServer(
|
|
||||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
requests.Add(1)
|
|
||||||
w.WriteHeader(tt.status)
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
err := getNothing(testClient(), server.URL, 10*time.Second)
|
|
||||||
require.ErrorIs(t, err, errHTTPStatus)
|
|
||||||
require.EqualError(t, err, fmt.Sprintf("HTTP %d", tt.status))
|
|
||||||
assert.Equal(t, tt.requests, requests.Load())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGetTimesOutOnStalledServer points get at a server that takes a
|
|
||||||
// request and never answers it. The try must give up at the client's
|
|
||||||
// timeout and be retried; when every try stalls, get must return a
|
|
||||||
// timeout error rather than hang.
|
|
||||||
func TestGetTimesOutOnStalledServer(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
stallEvery bool
|
|
||||||
}{
|
|
||||||
{"first request stalls", false},
|
|
||||||
{"every request stalls", true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var requests atomic.Int32
|
|
||||||
|
|
||||||
stop := make(chan struct{})
|
|
||||||
server := httptest.NewServer(
|
|
||||||
http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
|
|
||||||
if requests.Add(1) == 1 || tt.stallEvery {
|
|
||||||
select {
|
|
||||||
case <-r.Context().Done():
|
|
||||||
case <-stop:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
defer server.Close()
|
|
||||||
defer close(stop)
|
|
||||||
|
|
||||||
client := testClient()
|
|
||||||
client.client.Timeout = 50 * time.Millisecond
|
|
||||||
|
|
||||||
err := getNothing(client, server.URL, 10*time.Second)
|
|
||||||
if !tt.stallEvery {
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var netErr net.Error
|
|
||||||
require.ErrorAs(t, err, &netErr)
|
|
||||||
assert.True(t, netErr.Timeout(), "want a timeout, got %v", err)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGetHonorsRetryAfter answers the first request with a 503 and a
|
|
||||||
// Retry-After header, and any later one with 200 OK. The client's own
|
|
||||||
// wait before a retry is an hour, so get finishes in time only if it
|
|
||||||
// waits as long as Retry-After says instead. A Retry-After longer than
|
|
||||||
// maxRetryAfter must fail at once.
|
|
||||||
func TestGetHonorsRetryAfter(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
value string
|
|
||||||
requests int32
|
|
||||||
wantErr bool
|
|
||||||
}{
|
|
||||||
{"seconds", "0", 2, false},
|
|
||||||
{"HTTP date", time.Now().Add(-time.Minute).UTC().Format(http.TimeFormat), 2, false},
|
|
||||||
{"too long", strconv.Itoa(int((maxRetryAfter + time.Second).Seconds())), 1, true},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var requests atomic.Int32
|
|
||||||
|
|
||||||
server := httptest.NewServer(
|
|
||||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
if requests.Add(1) == 1 {
|
|
||||||
w.Header().Set("Retry-After", tt.value)
|
|
||||||
w.WriteHeader(http.StatusServiceUnavailable)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
client := testClient()
|
|
||||||
client.firstDelay = time.Hour
|
|
||||||
|
|
||||||
err := getNothing(client, server.URL, 10*time.Second)
|
|
||||||
if tt.wantErr {
|
|
||||||
require.ErrorIs(t, err, errHTTPStatus)
|
|
||||||
} else {
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Equal(t, tt.requests, requests.Load())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGetStopsWaitingWhenCanceled gives get a server that always answers
|
|
||||||
// 503 and an hour's wait before each retry, then lets the context expire
|
|
||||||
// during that wait. get returning at all shows it stopped waiting.
|
|
||||||
func TestGetStopsWaitingWhenCanceled(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
server := httptest.NewServer(
|
|
||||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
w.WriteHeader(http.StatusServiceUnavailable)
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
client := testClient()
|
|
||||||
client.firstDelay = time.Hour
|
|
||||||
|
|
||||||
err := getNothing(client, server.URL, 50*time.Millisecond)
|
|
||||||
require.ErrorIs(t, err, context.DeadlineExceeded)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDownloadFileRetriesToSuccess serves a file that fails twice before
|
|
||||||
// it succeeds: first with a 503, then with a body cut off halfway. The
|
|
||||||
// download must end with the whole, verified file and no temp file.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest // changes the process-global working directory
|
|
||||||
func TestDownloadFileRetriesToSuccess(t *testing.T) {
|
|
||||||
content := []byte("the whole file, every byte of it")
|
|
||||||
|
|
||||||
manifest, err := mfer.NewManifestFromReader(bytes.NewReader(
|
|
||||||
manifestOf(t, map[string][]byte{testFileTxt: content})))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
var requests atomic.Int32
|
|
||||||
|
|
||||||
server := httptest.NewServer(
|
|
||||||
http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
switch requests.Add(1) {
|
|
||||||
case 1:
|
|
||||||
w.WriteHeader(http.StatusServiceUnavailable)
|
|
||||||
case 2:
|
|
||||||
// Promise the whole file but send half of it; the server
|
|
||||||
// then closes the connection.
|
|
||||||
w.Header().Set("Content-Length", strconv.Itoa(len(content)))
|
|
||||||
_, _ = w.Write(content[:len(content)/2])
|
|
||||||
default:
|
|
||||||
_, _ = w.Write(content)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
chdirTemp(t)
|
|
||||||
|
|
||||||
err = downloadFile(context.Background(), testClient(),
|
|
||||||
server.URL+"/"+testFileTxt, testFileTxt, manifest.Files()[0], nil)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, int32(3), requests.Load())
|
|
||||||
|
|
||||||
fetched, err := os.ReadFile(testFileTxt)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, content, fetched)
|
|
||||||
|
|
||||||
_, err = os.Stat(".file.txt.tmp")
|
|
||||||
assert.True(t, os.IsNotExist(err), "temp file left behind")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFetchBaseURLForms fetches one tree by its directory URL with and
|
|
||||||
// without a trailing slash, with a query string, and by its manifest URL.
|
|
||||||
// Each must request the same manifest and file paths.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest // changes the process-global working directory
|
|
||||||
func TestFetchBaseURLForms(t *testing.T) {
|
|
||||||
files := map[string][]byte{"one.txt": []byte("1"), "sub/two.txt": []byte("2")}
|
|
||||||
tree := http.StripPrefix("/tree", fetchTestHandler(manifestOf(t, files), files))
|
|
||||||
|
|
||||||
var (
|
|
||||||
mu sync.Mutex
|
|
||||||
requested []string
|
|
||||||
)
|
|
||||||
|
|
||||||
server := httptest.NewServer(
|
|
||||||
http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
mu.Lock()
|
|
||||||
|
|
||||||
requested = append(requested, r.URL.Path)
|
|
||||||
|
|
||||||
mu.Unlock()
|
|
||||||
|
|
||||||
tree.ServeHTTP(w, r)
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
inputs := []string{"/tree", "/tree/", "/tree?key=value", "/tree/index.mf"}
|
|
||||||
for _, input := range inputs {
|
|
||||||
t.Run(input, func(t *testing.T) {
|
|
||||||
mu.Lock()
|
|
||||||
requested = nil
|
|
||||||
mu.Unlock()
|
|
||||||
|
|
||||||
chdirTemp(t)
|
|
||||||
|
|
||||||
opts := testOpts([]string{testApp, cmdFetch, "-q", server.URL + input},
|
|
||||||
afero.NewOsFs())
|
|
||||||
require.Equal(t, 0, runCLI(opts), testStderr(t, opts))
|
|
||||||
|
|
||||||
mu.Lock()
|
|
||||||
defer mu.Unlock()
|
|
||||||
|
|
||||||
assert.ElementsMatch(t,
|
|
||||||
[]string{"/tree/index.mf", "/tree/one.txt", "/tree/sub/two.txt"}, requested)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFetchEscapedPaths fetches files whose names need escaping in a URL,
|
|
||||||
// one of them a name that is itself an escape sequence. Each must arrive
|
|
||||||
// under its own name with its own content.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest // changes the process-global working directory
|
|
||||||
func TestFetchEscapedPaths(t *testing.T) {
|
|
||||||
files := map[string][]byte{
|
|
||||||
"a b.txt": []byte("space"),
|
|
||||||
"a%20b.txt": []byte("percent sign, two, zero"),
|
|
||||||
"100%.txt": []byte("percent sign"),
|
|
||||||
"q?x=1#frag.txt": []byte("question mark and hash"),
|
|
||||||
"dir with space/sub.txt": []byte("directory with a space"),
|
|
||||||
}
|
|
||||||
|
|
||||||
server := httptest.NewServer(fetchTestHandler(manifestOf(t, files), files))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
chdirTemp(t)
|
|
||||||
|
|
||||||
opts := testOpts([]string{testApp, cmdFetch, "-q", server.URL}, afero.NewOsFs())
|
|
||||||
require.Equal(t, 0, runCLI(opts), testStderr(t, opts))
|
|
||||||
|
|
||||||
for name, content := range files {
|
|
||||||
fetched, err := os.ReadFile(name) //nolint:gosec // test-controlled path
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, content, fetched, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFetchTimeoutFlag runs fetch with --timeout against a server that
|
|
||||||
// never answers. Without the flag's limit the request would wait forever;
|
|
||||||
// once fetch gives up on it, the server cancels fetch's context so that
|
|
||||||
// fetch returns instead of retrying.
|
|
||||||
func TestFetchTimeoutFlag(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
server := httptest.NewServer(
|
|
||||||
http.HandlerFunc(func(_ http.ResponseWriter, r *http.Request) {
|
|
||||||
<-r.Context().Done() // fetch gave up on this request
|
|
||||||
cancel()
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
mfa := &CLIApp{Fs: afero.NewMemMapFs()}
|
|
||||||
|
|
||||||
set := flag.NewFlagSet(cmdFetch, flag.ContinueOnError)
|
|
||||||
for _, f := range mfa.fetchCommand().Flags {
|
|
||||||
require.NoError(t, f.Apply(set))
|
|
||||||
}
|
|
||||||
|
|
||||||
require.NoError(t, set.Parse([]string{"--" + flagTimeout, "100ms", server.URL}))
|
|
||||||
|
|
||||||
cliCtx := urfcli.NewContext(nil, set, nil)
|
|
||||||
cliCtx.Context = ctx
|
|
||||||
|
|
||||||
// fetchManifestOperation logs to the process-global logger.
|
|
||||||
err := runLocked(func() error { return mfa.fetchManifestOperation(cliCtx) })
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFetchRejectsTimeoutOfZeroOrLess runs fetch with a --timeout of zero
|
|
||||||
// and of less than zero. http.Client takes either as no time limit at all,
|
|
||||||
// so fetch must refuse it before making any request.
|
|
||||||
func TestFetchRejectsTimeoutOfZeroOrLess(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var requests atomic.Int32
|
|
||||||
|
|
||||||
server := httptest.NewServer(
|
|
||||||
http.HandlerFunc(func(http.ResponseWriter, *http.Request) {
|
|
||||||
requests.Add(1)
|
|
||||||
}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
for _, timeout := range []string{"0", "-1s"} {
|
|
||||||
opts := testOpts(
|
|
||||||
[]string{testApp, cmdFetch, "--" + flagTimeout + "=" + timeout, server.URL},
|
|
||||||
afero.NewMemMapFs())
|
|
||||||
|
|
||||||
assert.Equal(t, 1, runCLI(opts), timeout)
|
|
||||||
assert.Contains(t, testStderr(t, opts), errInvalidTimeout.Error(), timeout)
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Zero(t, requests.Load(), "fetch made a request")
|
|
||||||
}
|
|
||||||
|
|||||||
+10
-35
@@ -7,7 +7,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -57,7 +56,7 @@ type freshenEntry struct {
|
|||||||
type freshenScanner struct {
|
type freshenScanner struct {
|
||||||
fs afero.Fs
|
fs afero.Fs
|
||||||
absBase string
|
absBase string
|
||||||
excluded []fs.FileInfo // files left out of the listing
|
manifestBase string
|
||||||
includeDotfiles bool
|
includeDotfiles bool
|
||||||
followSymlinks bool
|
followSymlinks bool
|
||||||
showProgress bool
|
showProgress bool
|
||||||
@@ -157,6 +156,11 @@ func (s *freshenScanner) walk(path string, info fs.FileInfo, walkErr error) erro
|
|||||||
"freshen: failed to compute relative path for %s: %w", path, err)
|
"freshen: failed to compute relative path for %s: %w", path, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Skip the manifest file itself
|
||||||
|
if relPath == s.manifestBase || relPath == "."+s.manifestBase {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Handle dotfiles
|
// Handle dotfiles
|
||||||
if !s.includeDotfiles && mfer.IsHiddenPath(filepath.ToSlash(relPath)) {
|
if !s.includeDotfiles && mfer.IsHiddenPath(filepath.ToSlash(relPath)) {
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
@@ -181,12 +185,6 @@ func (s *freshenScanner) walk(path string, info fs.FileInfo, walkErr error) erro
|
|||||||
info = realInfo
|
info = realInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, excluded := range s.excluded {
|
|
||||||
if os.SameFile(info, excluded) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
s.scanCount++
|
s.scanCount++
|
||||||
|
|
||||||
// Check against existing manifest
|
// Check against existing manifest
|
||||||
@@ -309,7 +307,7 @@ func (h *freshenHasher) processEntry(e *freshenEntry) error {
|
|||||||
func writeFreshenedManifest(
|
func writeFreshenedManifest(
|
||||||
ctx context.Context, afs afero.Fs, builder *mfer.Builder, manifestPath string,
|
ctx context.Context, afs afero.Fs, builder *mfer.Builder, manifestPath string,
|
||||||
) error {
|
) error {
|
||||||
tmpPath := manifestTempPath(manifestPath)
|
tmpPath := manifestPath + ".tmp"
|
||||||
|
|
||||||
outFile, err := afs.Create(tmpPath)
|
outFile, err := afs.Create(tmpPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -366,22 +364,10 @@ func (mfa *CLIApp) freshenScan(
|
|||||||
startScan := time.Now()
|
startScan := time.Now()
|
||||||
showProgress := ctx.Bool("progress")
|
showProgress := ctx.Bool("progress")
|
||||||
|
|
||||||
// Leave out the manifest and a temp file left by an interrupted run,
|
|
||||||
// as gen does. A path that cannot be stat'd, normally because no file
|
|
||||||
// is there, needs no leaving out.
|
|
||||||
var excluded []fs.FileInfo
|
|
||||||
|
|
||||||
for _, p := range []string{manifestPath, manifestTempPath(manifestPath)} {
|
|
||||||
info, err := mfa.Fs.Stat(p)
|
|
||||||
if err == nil {
|
|
||||||
excluded = append(excluded, info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
scanner := &freshenScanner{
|
scanner := &freshenScanner{
|
||||||
fs: mfa.Fs,
|
fs: mfa.Fs,
|
||||||
absBase: absBase,
|
absBase: absBase,
|
||||||
excluded: excluded,
|
manifestBase: filepath.Base(manifestPath),
|
||||||
includeDotfiles: ctx.Bool("include-dotfiles"),
|
includeDotfiles: ctx.Bool("include-dotfiles"),
|
||||||
followSymlinks: ctx.Bool("follow-symlinks"),
|
followSymlinks: ctx.Bool("follow-symlinks"),
|
||||||
showProgress: showProgress,
|
showProgress: showProgress,
|
||||||
@@ -459,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)
|
||||||
}
|
}
|
||||||
@@ -629,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,13 +4,10 @@ package cli
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"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"
|
||||||
@@ -32,7 +29,7 @@ func (s stubFileInfo) IsDir() bool { return false }
|
|||||||
func (s stubFileInfo) Sys() any { return nil }
|
func (s stubFileInfo) Sys() any { return nil }
|
||||||
|
|
||||||
// setupFreshenDir populates /testdir with two files, scans it, and
|
// setupFreshenDir populates /testdir with two files, scans it, and
|
||||||
// writes the resulting manifest to /testdir/index.mf.
|
// writes the resulting manifest to /testdir/.index.mf.
|
||||||
func setupFreshenDir(t *testing.T, fs afero.Fs) {
|
func setupFreshenDir(t *testing.T, fs afero.Fs) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
@@ -51,7 +48,7 @@ func setupFreshenDir(t *testing.T, fs afero.Fs) {
|
|||||||
|
|
||||||
// Write manifest to filesystem
|
// Write manifest to filesystem
|
||||||
require.NoError(t,
|
require.NoError(t,
|
||||||
afero.WriteFile(fs, "/testdir/index.mf", manifestBuf.Bytes(), 0o644))
|
afero.WriteFile(fs, "/testdir/.index.mf", manifestBuf.Bytes(), 0o644))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFreshenUnchanged(t *testing.T) {
|
func TestFreshenUnchanged(t *testing.T) {
|
||||||
@@ -61,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)
|
||||||
}
|
}
|
||||||
@@ -76,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)
|
||||||
|
|
||||||
@@ -104,64 +95,6 @@ func TestFreshenWithChanges(t *testing.T) {
|
|||||||
assert.Equal(t, "modified content2", string(content))
|
assert.Equal(t, "modified content2", string(content))
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestFreshenLeavesManifestOutOfListing freshens a manifest kept in a
|
|
||||||
// subdirectory of the tree it lists: the manifest is not listed, while an
|
|
||||||
// ordinary file of the same name at the top of the tree is. The manifest
|
|
||||||
// is recognized by file identity, which needs the real filesystem.
|
|
||||||
func TestFreshenLeavesManifestOutOfListing(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
root := t.TempDir()
|
|
||||||
manifestPath := filepath.Join(root, "sub", "listing.mf")
|
|
||||||
|
|
||||||
fs := afero.NewOsFs()
|
|
||||||
require.NoError(t, fs.MkdirAll(filepath.Join(root, "sub"), 0o750))
|
|
||||||
writeTestFile(t, fs, filepath.Join(root, testFileTxt), "hello")
|
|
||||||
writeTestFile(t, fs, filepath.Join(root, "listing.mf"), "an ordinary file")
|
|
||||||
|
|
||||||
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", manifestPath, root}, fs)
|
|
||||||
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
|
|
||||||
|
|
||||||
// A new file gives freshen something to write.
|
|
||||||
writeTestFile(t, fs, filepath.Join(root, "added.txt"), "added")
|
|
||||||
|
|
||||||
opts = testOpts([]string{
|
|
||||||
testApp, "freshen", "-q", testFlagBase, root, manifestPath,
|
|
||||||
}, fs)
|
|
||||||
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
|
|
||||||
|
|
||||||
assert.ElementsMatch(t, []string{testFileTxt, "listing.mf", "added.txt"},
|
|
||||||
manifestPaths(t, fs, manifestPath))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFreshenLeavesLeftoverTempFileOutOfListing freshens a manifest where
|
|
||||||
// an interrupted run left its temp file beside it: the leftover is not
|
|
||||||
// listed.
|
|
||||||
func TestFreshenLeavesLeftoverTempFileOutOfListing(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
root := t.TempDir()
|
|
||||||
manifestPath := filepath.Join(root, "index.mf")
|
|
||||||
|
|
||||||
fs := afero.NewOsFs()
|
|
||||||
writeTestFile(t, fs, filepath.Join(root, testFileTxt), "hello")
|
|
||||||
|
|
||||||
opts := testOpts([]string{testApp, cmdGenerate, "-q", "-o", manifestPath, root}, fs)
|
|
||||||
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
|
|
||||||
|
|
||||||
writeTestFile(t, fs, manifestTempPath(manifestPath), "part of a manifest")
|
|
||||||
// A new file gives freshen something to write.
|
|
||||||
writeTestFile(t, fs, filepath.Join(root, "added.txt"), "added")
|
|
||||||
|
|
||||||
opts = testOpts([]string{
|
|
||||||
testApp, "freshen", "-q", testFlagBase, root, manifestPath,
|
|
||||||
}, fs)
|
|
||||||
require.Equal(t, 0, runCLI(opts), "stderr: %s", testStderr(t, opts))
|
|
||||||
|
|
||||||
assert.ElementsMatch(t, []string{testFileTxt, "added.txt"},
|
|
||||||
manifestPaths(t, fs, manifestPath))
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFreshenRecordEntryMtimePresence pins the behavior of recordEntry
|
// TestFreshenRecordEntryMtimePresence pins the behavior of recordEntry
|
||||||
// with respect to MFFilePath.Mtime, which is a message pointer with
|
// with respect to MFFilePath.Mtime, which is a message pointer with
|
||||||
// proto3 field presence and may legitimately be absent.
|
// proto3 field presence and may legitimately be absent.
|
||||||
@@ -236,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()
|
||||||
|
|||||||
@@ -89,15 +89,11 @@ func (mfa *CLIApp) collectInputPaths(args cli.Args) ([]string, error) {
|
|||||||
|
|
||||||
// buildScannerOptions constructs scanner options from the CLI flags.
|
// buildScannerOptions constructs scanner options from the CLI flags.
|
||||||
func (mfa *CLIApp) buildScannerOptions(ctx *cli.Context) *mfer.ScannerOptions {
|
func (mfa *CLIApp) buildScannerOptions(ctx *cli.Context) *mfer.ScannerOptions {
|
||||||
output := ctx.String("output")
|
|
||||||
opts := &mfer.ScannerOptions{
|
opts := &mfer.ScannerOptions{
|
||||||
IncludeDotfiles: ctx.Bool("include-dotfiles"),
|
IncludeDotfiles: ctx.Bool("include-dotfiles"),
|
||||||
FollowSymLinks: ctx.Bool("follow-symlinks"),
|
FollowSymLinks: ctx.Bool("follow-symlinks"),
|
||||||
IncludeTimestamps: ctx.Bool("include-timestamps"),
|
IncludeTimestamps: ctx.Bool("include-timestamps"),
|
||||||
Fs: mfa.Fs,
|
Fs: mfa.Fs,
|
||||||
// Neither a manifest being replaced nor a temp file left by an
|
|
||||||
// interrupted run belongs in the new manifest.
|
|
||||||
ExcludePaths: []string{output, manifestTempPath(output)},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set seed for deterministic UUID if provided
|
// Set seed for deterministic UUID if provided
|
||||||
@@ -221,7 +217,7 @@ func (mfa *CLIApp) generateManifestOperation(ctx *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create temp file for atomic write
|
// Create temp file for atomic write
|
||||||
tmpPath := manifestTempPath(outputPath)
|
tmpPath := outputPath + ".tmp"
|
||||||
|
|
||||||
outFile, err := mfa.Fs.Create(tmpPath)
|
outFile, err := mfa.Fs.Create(tmpPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ func (mfa *CLIApp) openManifestReader(pathOrURL string) (io.ReadCloser, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resolveManifestArg resolves the manifest path from CLI arguments.
|
// resolveManifestArg resolves the manifest path from CLI arguments.
|
||||||
// HTTP(S) URLs are returned as-is. Directories are searched for index.mf.
|
// HTTP(S) URLs are returned as-is. Directories are searched for index.mf/.index.mf.
|
||||||
// If no argument is given, the current directory is searched.
|
// If no argument is given, the current directory is searched.
|
||||||
func (mfa *CLIApp) resolveManifestArg(ctx *cli.Context) (string, error) {
|
func (mfa *CLIApp) resolveManifestArg(ctx *cli.Context) (string, error) {
|
||||||
if ctx.Args().Len() > 0 {
|
if ctx.Args().Len() > 0 {
|
||||||
|
|||||||
@@ -17,28 +17,15 @@ 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"
|
||||||
|
|
||||||
flagProgress = "progress"
|
flagProgress = "progress"
|
||||||
flagTimeout = "timeout"
|
|
||||||
|
|
||||||
manifestArgsUsage = "[manifest file]"
|
manifestArgsUsage = "[manifest file]"
|
||||||
|
|
||||||
// defaultManifestName is the filename gen writes by default, the one
|
|
||||||
// looked for when a command is given a directory, and the one fetch
|
|
||||||
// appends to a directory URL.
|
|
||||||
defaultManifestName = "index.mf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// manifestTempPath returns the temp file gen and freshen write a manifest
|
|
||||||
// to before renaming it to out.
|
|
||||||
func manifestTempPath(out string) string {
|
|
||||||
return out + ".tmp"
|
|
||||||
}
|
|
||||||
|
|
||||||
// errUnknownCommand indicates an unrecognized command argument.
|
// errUnknownCommand indicates an unrecognized command argument.
|
||||||
var errUnknownCommand = errors.New("unknown command")
|
var errUnknownCommand = errors.New("unknown command")
|
||||||
|
|
||||||
@@ -131,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{
|
||||||
@@ -144,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()
|
||||||
@@ -168,7 +154,7 @@ func (mfa *CLIApp) generateCommand() *cli.Command {
|
|||||||
},
|
},
|
||||||
&cli.StringFlag{
|
&cli.StringFlag{
|
||||||
Name: "output",
|
Name: "output",
|
||||||
Value: defaultManifestName,
|
Value: "./.index.mf",
|
||||||
Aliases: []string{"o"},
|
Aliases: []string{"o"},
|
||||||
Usage: "Specify output filename",
|
Usage: "Specify output filename",
|
||||||
},
|
},
|
||||||
@@ -241,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 {
|
||||||
@@ -338,23 +324,15 @@ 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()
|
||||||
|
|
||||||
return mfa.fetchManifestOperation(c)
|
return mfa.fetchManifestOperation(c)
|
||||||
},
|
},
|
||||||
Flags: append(commonFlags(),
|
Flags: commonFlags(),
|
||||||
&cli.DurationFlag{
|
|
||||||
Name: flagTimeout,
|
|
||||||
Value: httpTimeout,
|
|
||||||
Usage: "Time limit for each HTTP request, including the download " +
|
|
||||||
"of its body",
|
|
||||||
},
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+55
-149
@@ -1,25 +1,19 @@
|
|||||||
// Package log provides leveled logging on top of log/slog, and helpers that
|
// Package log provides leveled logging with progress output helpers
|
||||||
// print progress lines which overwrite each other in place on a terminal.
|
// on top of apex/log and pterm.
|
||||||
//
|
|
||||||
// Until Init runs, log records go to slog.Default(), so a program that uses
|
|
||||||
// package mfer as a library gets them wherever it sends its own slog output.
|
|
||||||
// Init switches to the CLI's format on the stderr writer given to SetOutput.
|
|
||||||
// Progress lines are not log records: they are written straight to the stdout
|
|
||||||
// writer, never through slog.
|
|
||||||
package log
|
package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/apex/log"
|
||||||
|
acli "github.com/apex/log/handlers/cli"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
"golang.org/x/term"
|
"github.com/pterm/pterm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Level represents log severity levels.
|
// Level represents log severity levels.
|
||||||
@@ -64,98 +58,28 @@ func (l Level) String() string {
|
|||||||
// helpers to the caller of the log package.
|
// helpers to the caller of the log package.
|
||||||
const callerSkip = 2
|
const callerSkip = 2
|
||||||
|
|
||||||
// Escape sequences for colored log lines.
|
|
||||||
const (
|
|
||||||
ansiReset = "\x1b[0m"
|
|
||||||
ansiBold = "\x1b[1m"
|
|
||||||
ansiRed = "\x1b[31m"
|
|
||||||
ansiYellow = "\x1b[33m"
|
|
||||||
ansiBlue = "\x1b[34m"
|
|
||||||
ansiWhite = "\x1b[37m"
|
|
||||||
)
|
|
||||||
|
|
||||||
//nolint:gochecknoglobals // package-level logger state by design
|
//nolint:gochecknoglobals // package-level logger state by design
|
||||||
var (
|
var (
|
||||||
// mu protects the variables below
|
// mu protects the output writers and level
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
// stdout is the writer for progress output
|
// stdout is the writer for progress output
|
||||||
stdout io.Writer = os.Stdout
|
stdout io.Writer = os.Stdout
|
||||||
// stderr is the writer for log output
|
// stderr is the writer for log output
|
||||||
stderr io.Writer = os.Stderr
|
stderr io.Writer = os.Stderr
|
||||||
// styled is false once DisableStyling has been called
|
|
||||||
styled = true
|
|
||||||
// logger is the CLI logger Init builds; nil until Init runs
|
|
||||||
logger *slog.Logger
|
|
||||||
// currentLevel is our log level (includes Verbose)
|
// currentLevel is our log level (includes Verbose)
|
||||||
currentLevel = InfoLevel
|
currentLevel = InfoLevel
|
||||||
)
|
)
|
||||||
|
|
||||||
// cliHandler is the slog.Handler the CLI logs through. Each record is one
|
|
||||||
// line: a symbol for its level right-aligned in four columns, then the
|
|
||||||
// message padded to 25 columns. In color, only the symbol is bold and in the
|
|
||||||
// level's color; the message is in the terminal's default color. Records are
|
|
||||||
// filtered by level before they are made, so the handler takes every record
|
|
||||||
// it is given.
|
|
||||||
type cliHandler struct {
|
|
||||||
mu sync.Mutex
|
|
||||||
w io.Writer
|
|
||||||
color bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// Enabled reports true for every level.
|
|
||||||
func (h *cliHandler) Enabled(context.Context, slog.Level) bool {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle writes the record as one line.
|
|
||||||
func (h *cliHandler) Handle(_ context.Context, r slog.Record) error {
|
|
||||||
symbol, color := "•", ansiBlue
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case r.Level >= slog.LevelError:
|
|
||||||
symbol, color = "⨯", ansiRed
|
|
||||||
case r.Level >= slog.LevelWarn:
|
|
||||||
color = ansiYellow
|
|
||||||
case r.Level < slog.LevelInfo:
|
|
||||||
color = ansiWhite
|
|
||||||
}
|
|
||||||
|
|
||||||
h.mu.Lock()
|
|
||||||
defer h.mu.Unlock()
|
|
||||||
|
|
||||||
if !h.color {
|
|
||||||
_, err := fmt.Fprintf(h.w, "%4s %-25s\n", symbol, r.Message)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := fmt.Fprintf(h.w, "%s%s%4s%s %-25s%s\n",
|
|
||||||
color, ansiBold, symbol, ansiReset, r.Message, ansiReset)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithAttrs returns the handler unchanged: this package's helpers never
|
|
||||||
// attach attributes.
|
|
||||||
func (h *cliHandler) WithAttrs([]slog.Attr) slog.Handler {
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithGroup returns the handler unchanged: this package's helpers never open
|
|
||||||
// groups.
|
|
||||||
func (h *cliHandler) WithGroup(string) slog.Handler {
|
|
||||||
return h
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetOutput configures the output writers for the log package.
|
// SetOutput configures the output writers for the log package.
|
||||||
// stdout is used for progress output, stderr is used for log messages
|
// stdout is used for progress output, stderr is used for log messages.
|
||||||
// from the next Init on.
|
|
||||||
func SetOutput(out, err io.Writer) {
|
func SetOutput(out, err io.Writer) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|
||||||
stdout = out
|
stdout = out
|
||||||
stderr = err
|
stderr = err
|
||||||
|
|
||||||
|
pterm.SetDefaultOutput(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetStdout returns the configured stdout writer.
|
// GetStdout returns the configured stdout writer.
|
||||||
@@ -174,29 +98,31 @@ func GetStderr() io.Writer {
|
|||||||
return stderr
|
return stderr
|
||||||
}
|
}
|
||||||
|
|
||||||
// DisableStyling turns off colors in log lines from the next Init on.
|
// DisableStyling turns off colors and styling for terminal output.
|
||||||
func DisableStyling() {
|
func DisableStyling() {
|
||||||
mu.Lock()
|
pterm.DisableColor()
|
||||||
defer mu.Unlock()
|
pterm.DisableStyling()
|
||||||
|
|
||||||
styled = false
|
pterm.Debug.Prefix.Text = ""
|
||||||
|
pterm.Info.Prefix.Text = ""
|
||||||
|
pterm.Success.Prefix.Text = ""
|
||||||
|
pterm.Warning.Prefix.Text = ""
|
||||||
|
pterm.Error.Prefix.Text = ""
|
||||||
|
pterm.Fatal.Prefix.Text = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init sends log records to the CLI handler on the stderr writer given to
|
// Init initializes the logger with the CLI handler and default log level.
|
||||||
// SetOutput. Log lines are colored when stdout is a terminal whose TERM is
|
|
||||||
// not dumb, unless DisableStyling has been called.
|
|
||||||
//
|
//
|
||||||
// It replaces the logger under the write lock, and log calls hold the read
|
// It reconfigures the process-global apex/log logger under the write lock so
|
||||||
// lock while they write, so once Init returns nothing writes through the
|
// the global is never mutated while another goroutine holds the read lock to
|
||||||
// logger it replaced.
|
// read it in emit. Without this, parallel callers (e.g. the test suite) race
|
||||||
|
// Init's SetLevel/SetHandler against concurrent log calls.
|
||||||
func Init() {
|
func Init() {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
|
|
||||||
color := styled && os.Getenv("TERM") != "dumb" &&
|
log.SetHandler(acli.New(stderr))
|
||||||
term.IsTerminal(int(os.Stdout.Fd()))
|
log.SetLevel(log.DebugLevel) // Let apex/log pass everything; we filter ourselves
|
||||||
|
|
||||||
logger = slog.New(&cliHandler{w: stderr, color: color})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// isEnabled returns true if messages at the given level should be logged.
|
// isEnabled returns true if messages at the given level should be logged.
|
||||||
@@ -207,87 +133,66 @@ func isEnabled(l Level) bool {
|
|||||||
return l >= currentLevel
|
return l >= currentLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
// logf logs a formatted message at level l if messages at l are enabled,
|
// emit calls fn while holding the read lock if messages at level l are
|
||||||
// holding the read lock while the record is written. slog has no verbose or
|
// enabled. Holding the read lock across the apex/log call keeps the global
|
||||||
// fatal level: verbose messages are logged as info records, and fatal
|
// logger from being read while Init reconfigures it under the write lock.
|
||||||
// messages as error records.
|
func emit(l Level, fn func()) {
|
||||||
func logf(l Level, format string, args ...any) {
|
|
||||||
mu.RLock()
|
mu.RLock()
|
||||||
defer mu.RUnlock()
|
defer mu.RUnlock()
|
||||||
|
|
||||||
if l < currentLevel {
|
if l >= currentLevel {
|
||||||
return
|
fn()
|
||||||
}
|
|
||||||
|
|
||||||
lg := logger
|
|
||||||
if lg == nil {
|
|
||||||
lg = slog.Default()
|
|
||||||
}
|
|
||||||
|
|
||||||
msg := fmt.Sprintf(format, args...)
|
|
||||||
|
|
||||||
switch l {
|
|
||||||
case DebugLevel:
|
|
||||||
lg.Debug(msg)
|
|
||||||
case VerboseLevel, InfoLevel:
|
|
||||||
lg.Info(msg)
|
|
||||||
case WarnLevel:
|
|
||||||
lg.Warn(msg)
|
|
||||||
case ErrorLevel, FatalLevel:
|
|
||||||
lg.Error(msg)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatalf logs a formatted message at fatal level, then exits with status 1.
|
// Fatalf logs a formatted message at fatal level.
|
||||||
func Fatalf(format string, args ...any) {
|
func Fatalf(format string, args ...any) {
|
||||||
logf(FatalLevel, format, args...)
|
emit(FatalLevel, func() { log.Fatalf(format, args...) })
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatal logs a message at fatal level, then exits with status 1.
|
// Fatal logs a message at fatal level.
|
||||||
func Fatal(arg string) {
|
func Fatal(arg string) {
|
||||||
logf(FatalLevel, "%s", arg)
|
emit(FatalLevel, func() { log.Fatal(arg) })
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Errorf logs a formatted message at error level.
|
// Errorf logs a formatted message at error level.
|
||||||
func Errorf(format string, args ...any) {
|
func Errorf(format string, args ...any) {
|
||||||
logf(ErrorLevel, format, args...)
|
emit(ErrorLevel, func() { log.Errorf(format, args...) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Error logs a message at error level.
|
// Error logs a message at error level.
|
||||||
func Error(arg string) {
|
func Error(arg string) {
|
||||||
logf(ErrorLevel, "%s", arg)
|
emit(ErrorLevel, func() { log.Error(arg) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warnf logs a formatted message at warn level.
|
// Warnf logs a formatted message at warn level.
|
||||||
func Warnf(format string, args ...any) {
|
func Warnf(format string, args ...any) {
|
||||||
logf(WarnLevel, format, args...)
|
emit(WarnLevel, func() { log.Warnf(format, args...) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Warn logs a message at warn level.
|
// Warn logs a message at warn level.
|
||||||
func Warn(arg string) {
|
func Warn(arg string) {
|
||||||
logf(WarnLevel, "%s", arg)
|
emit(WarnLevel, func() { log.Warn(arg) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Infof logs a formatted message at info level.
|
// Infof logs a formatted message at info level.
|
||||||
func Infof(format string, args ...any) {
|
func Infof(format string, args ...any) {
|
||||||
logf(InfoLevel, format, args...)
|
emit(InfoLevel, func() { log.Infof(format, args...) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Info logs a message at info level.
|
// Info logs a message at info level.
|
||||||
func Info(arg string) {
|
func Info(arg string) {
|
||||||
logf(InfoLevel, "%s", arg)
|
emit(InfoLevel, func() { log.Info(arg) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verbosef logs a formatted message at verbose level.
|
// Verbosef logs a formatted message at verbose level.
|
||||||
func Verbosef(format string, args ...any) {
|
func Verbosef(format string, args ...any) {
|
||||||
logf(VerboseLevel, format, args...)
|
emit(VerboseLevel, func() { log.Infof(format, args...) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verbose logs a message at verbose level.
|
// Verbose logs a message at verbose level.
|
||||||
func Verbose(arg string) {
|
func Verbose(arg string) {
|
||||||
logf(VerboseLevel, "%s", arg)
|
emit(VerboseLevel, func() { log.Info(arg) })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Debugf logs a formatted message at debug level with caller location.
|
// Debugf logs a formatted message at debug level with caller location.
|
||||||
@@ -306,12 +211,20 @@ func Debug(arg string) {
|
|||||||
|
|
||||||
// DebugReal logs at debug level with caller info from the specified stack depth.
|
// DebugReal logs at debug level with caller info from the specified stack depth.
|
||||||
func DebugReal(arg string, cs int) {
|
func DebugReal(arg string, cs int) {
|
||||||
|
mu.RLock()
|
||||||
|
defer mu.RUnlock()
|
||||||
|
|
||||||
|
if DebugLevel < currentLevel {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
_, callerFile, callerLine, ok := runtime.Caller(cs)
|
_, callerFile, callerLine, ok := runtime.Caller(cs)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
logf(DebugLevel, "%s:%d: %s", filepath.Base(callerFile), callerLine, arg)
|
tag := fmt.Sprintf("%s:%d: ", filepath.Base(callerFile), callerLine)
|
||||||
|
log.Debug(tag + arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dump logs a spew dump of the arguments at debug level.
|
// Dump logs a spew dump of the arguments at debug level.
|
||||||
@@ -362,19 +275,12 @@ func GetLevel() Level {
|
|||||||
|
|
||||||
// Progressf prints a progress message that overwrites the current line.
|
// Progressf prints a progress message that overwrites the current line.
|
||||||
// Use ProgressDone() when progress is complete to move to the next line.
|
// Use ProgressDone() when progress is complete to move to the next line.
|
||||||
// Progress goes to the stdout writer whatever the log level.
|
|
||||||
func Progressf(format string, args ...any) {
|
func Progressf(format string, args ...any) {
|
||||||
mu.Lock()
|
pterm.Printf("\r"+format, args...)
|
||||||
defer mu.Unlock()
|
|
||||||
|
|
||||||
_, _ = fmt.Fprintf(stdout, "\r"+format, args...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProgressDone clears the progress line when progress is complete.
|
// ProgressDone clears the progress line when progress is complete.
|
||||||
func ProgressDone() {
|
func ProgressDone() {
|
||||||
mu.Lock()
|
// Clear the line with spaces and return to beginning
|
||||||
defer mu.Unlock()
|
pterm.Print("\r\033[K")
|
||||||
|
|
||||||
// Return to the start of the line and erase it
|
|
||||||
_, _ = fmt.Fprint(stdout, "\r\033[K")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,229 +1,12 @@
|
|||||||
//nolint:testpackage // white-box tests exercise unexported internals
|
package log_test
|
||||||
package log
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
stdlog "log"
|
|
||||||
"log/slog"
|
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"sneak.berlin/go/mfer/internal/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestBuild(t *testing.T) {
|
func TestBuild(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
Init()
|
log.Init()
|
||||||
}
|
|
||||||
|
|
||||||
// capture points the package's writers at fresh buffers and sets its level,
|
|
||||||
// with colors off, then restores the default writers and level when the test
|
|
||||||
// ends. The state it changes is process-wide, so tests that use it do not
|
|
||||||
// run in parallel.
|
|
||||||
func capture(t *testing.T, l Level) (*bytes.Buffer, *bytes.Buffer) {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
var stdout, stderr bytes.Buffer
|
|
||||||
|
|
||||||
DisableStyling()
|
|
||||||
SetOutput(&stdout, &stderr)
|
|
||||||
SetLevel(l)
|
|
||||||
Init()
|
|
||||||
|
|
||||||
t.Cleanup(func() {
|
|
||||||
SetOutput(os.Stdout, os.Stderr)
|
|
||||||
SetLevel(InfoLevel)
|
|
||||||
Init()
|
|
||||||
})
|
|
||||||
|
|
||||||
return &stdout, &stderr
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLevelFiltering logs at every level, through both the plain and the
|
|
||||||
// formatting helpers, and checks that exactly the messages at or above the
|
|
||||||
// set level are written.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest // changes the package's process-wide writers and level
|
|
||||||
func TestLevelFiltering(t *testing.T) {
|
|
||||||
messages := []struct {
|
|
||||||
level Level
|
|
||||||
text string
|
|
||||||
}{
|
|
||||||
{DebugLevel, "debug plain"},
|
|
||||||
{DebugLevel, "debug formatted"},
|
|
||||||
{VerboseLevel, "verbose plain"},
|
|
||||||
{VerboseLevel, "verbose formatted"},
|
|
||||||
{InfoLevel, "info plain"},
|
|
||||||
{InfoLevel, "info formatted"},
|
|
||||||
{WarnLevel, "warn plain"},
|
|
||||||
{WarnLevel, "warn formatted"},
|
|
||||||
{ErrorLevel, "error plain"},
|
|
||||||
{ErrorLevel, "error formatted"},
|
|
||||||
}
|
|
||||||
|
|
||||||
levels := []Level{DebugLevel, VerboseLevel, InfoLevel, WarnLevel, ErrorLevel}
|
|
||||||
|
|
||||||
for _, set := range levels {
|
|
||||||
t.Run(set.String(), func(t *testing.T) {
|
|
||||||
stdout, stderr := capture(t, set)
|
|
||||||
|
|
||||||
Debug("debug plain")
|
|
||||||
Debugf("debug %s", "formatted")
|
|
||||||
Verbose("verbose plain")
|
|
||||||
Verbosef("verbose %s", "formatted")
|
|
||||||
Info("info plain")
|
|
||||||
Infof("info %s", "formatted")
|
|
||||||
Warn("warn plain")
|
|
||||||
Warnf("warn %s", "formatted")
|
|
||||||
Error("error plain")
|
|
||||||
Errorf("error %s", "formatted")
|
|
||||||
|
|
||||||
for _, m := range messages {
|
|
||||||
if m.level >= set {
|
|
||||||
assert.Contains(t, stderr.String(), m.text)
|
|
||||||
} else {
|
|
||||||
assert.NotContains(t, stderr.String(), m.text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert.Empty(t, stdout.String())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestLineFormat checks the exact uncolored lines: a symbol per level
|
|
||||||
// right-aligned in four columns, then the message padded to 25 columns.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest // changes the package's process-wide writers and level
|
|
||||||
func TestLineFormat(t *testing.T) {
|
|
||||||
_, stderr := capture(t, VerboseLevel)
|
|
||||||
|
|
||||||
Infof("scanning filesystem...")
|
|
||||||
Verbosef("+ %s (%s)", "a.txt", "1 B")
|
|
||||||
Warn("short")
|
|
||||||
Errorf("%s", "a message longer than twenty-five columns")
|
|
||||||
|
|
||||||
want := " • scanning filesystem... \n" +
|
|
||||||
" • + a.txt (1 B) \n" +
|
|
||||||
" • short \n" +
|
|
||||||
" ⨯ a message longer than twenty-five columns\n"
|
|
||||||
assert.Equal(t, want, stderr.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestDebugCallerTag checks that debug lines start with the file and line
|
|
||||||
// of the call that logged them.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest // changes the package's process-wide writers and level
|
|
||||||
func TestDebugCallerTag(t *testing.T) {
|
|
||||||
_, stderr := capture(t, DebugLevel)
|
|
||||||
|
|
||||||
Debugf("enumerating path: %s", "/tmp")
|
|
||||||
|
|
||||||
assert.Regexp(t, `^ • log_test\.go:\d+: enumerating path: /tmp\s*\n$`,
|
|
||||||
stderr.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestColoredLine checks the colored lines: only the symbol is bold and in the
|
|
||||||
// level's color; the message is in the terminal's default color.
|
|
||||||
func TestColoredLine(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
lg := slog.New(&cliHandler{w: &buf, color: true})
|
|
||||||
lg.Debug("debug")
|
|
||||||
lg.Info("info")
|
|
||||||
lg.Warn("warn")
|
|
||||||
lg.Error("error")
|
|
||||||
|
|
||||||
want := "\x1b[37m\x1b[1m •\x1b[0m debug \x1b[0m\n" +
|
|
||||||
"\x1b[34m\x1b[1m •\x1b[0m info \x1b[0m\n" +
|
|
||||||
"\x1b[33m\x1b[1m •\x1b[0m warn \x1b[0m\n" +
|
|
||||||
"\x1b[31m\x1b[1m ⨯\x1b[0m error \x1b[0m\n"
|
|
||||||
assert.Equal(t, want, buf.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestRecordLevels checks the slog level of the record each helper logs,
|
|
||||||
// through slog's text handler with the time left out. slog has no verbose
|
|
||||||
// level, so verbose messages are info records.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest // changes the package's process-wide logger and level
|
|
||||||
func TestRecordLevels(t *testing.T) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
capture(t, DebugLevel)
|
|
||||||
|
|
||||||
opts := &slog.HandlerOptions{
|
|
||||||
Level: slog.LevelDebug,
|
|
||||||
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
|
|
||||||
if a.Key == slog.TimeKey {
|
|
||||||
return slog.Attr{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return a
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
mu.Lock()
|
|
||||||
logger = slog.New(slog.NewTextHandler(&buf, opts))
|
|
||||||
mu.Unlock()
|
|
||||||
|
|
||||||
Debugf("debug")
|
|
||||||
Verbosef("verbose")
|
|
||||||
Infof("info")
|
|
||||||
Warnf("warn")
|
|
||||||
Errorf("error")
|
|
||||||
|
|
||||||
assert.Regexp(t, `^level=DEBUG msg="log_test\.go:\d+: debug"\n`+
|
|
||||||
"level=INFO msg=verbose\n"+
|
|
||||||
"level=INFO msg=info\n"+
|
|
||||||
"level=WARN msg=warn\n"+
|
|
||||||
"level=ERROR msg=error\n$", buf.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestProgress checks that progress lines go to the stdout writer whatever
|
|
||||||
// the log level, each starting with a carriage return so it overwrites the
|
|
||||||
// last, and that ProgressDone erases the line.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest // changes the package's process-wide writers and level
|
|
||||||
func TestProgress(t *testing.T) {
|
|
||||||
stdout, stderr := capture(t, ErrorLevel)
|
|
||||||
|
|
||||||
Progressf("Scanning: %d files found", 1000)
|
|
||||||
Progressf("Scanning: %d files found", 2000)
|
|
||||||
ProgressDone()
|
|
||||||
|
|
||||||
assert.Equal(t,
|
|
||||||
"\rScanning: 1000 files found\rScanning: 2000 files found\r\x1b[K",
|
|
||||||
stdout.String())
|
|
||||||
assert.Empty(t, stderr.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestBeforeInit checks that records go to slog.Default() until Init runs,
|
|
||||||
// still filtered by the package's level. slog's default handler writes
|
|
||||||
// through the standard library's log package.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest // changes the package's process-wide logger
|
|
||||||
func TestBeforeInit(t *testing.T) {
|
|
||||||
var buf bytes.Buffer
|
|
||||||
|
|
||||||
flags := stdlog.Flags()
|
|
||||||
|
|
||||||
stdlog.SetOutput(&buf)
|
|
||||||
stdlog.SetFlags(0)
|
|
||||||
|
|
||||||
mu.Lock()
|
|
||||||
logger = nil
|
|
||||||
mu.Unlock()
|
|
||||||
|
|
||||||
t.Cleanup(func() {
|
|
||||||
stdlog.SetOutput(os.Stderr)
|
|
||||||
stdlog.SetFlags(flags)
|
|
||||||
Init()
|
|
||||||
})
|
|
||||||
|
|
||||||
Infof("loaded manifest with %d files", 3)
|
|
||||||
Verbose("not shown at info level")
|
|
||||||
|
|
||||||
assert.Equal(t, "INFO loaded manifest with 3 files\n", buf.String())
|
|
||||||
}
|
}
|
||||||
|
|||||||
+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
-75
@@ -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,58 +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)
|
|
||||||
|
|
||||||
truncatedHash, err := multihash.Encode(make([]byte, sha256.Size-1), multihash.SHA2_256)
|
|
||||||
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},
|
|
||||||
// A valid multihash whose 31-byte digest is one byte short.
|
|
||||||
{"31-byte digest", truncatedHash, 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()
|
||||||
|
|
||||||
@@ -380,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()
|
||||||
|
|
||||||
@@ -422,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
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-196
@@ -5,8 +5,6 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -16,13 +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"
|
|
||||||
// testDataManifestPath is a manifest kept inside the checked tree.
|
|
||||||
testDataManifestPath = testDataDir + "/index.mf"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStatusString(t *testing.T) {
|
func TestStatusString(t *testing.T) {
|
||||||
@@ -72,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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,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())
|
||||||
@@ -110,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)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -123,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()
|
||||||
|
|
||||||
@@ -199,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())
|
||||||
@@ -220,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)
|
||||||
@@ -254,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)
|
||||||
@@ -297,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)
|
||||||
@@ -327,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")
|
||||||
@@ -335,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)
|
||||||
@@ -359,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)
|
||||||
@@ -403,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())
|
||||||
@@ -429,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{
|
||||||
@@ -437,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)
|
||||||
@@ -466,7 +360,7 @@ func TestFindExtraFilesSkipsManifestAndDotfiles(t *testing.T) {
|
|||||||
manifestFiles := map[string][]byte{
|
manifestFiles := map[string][]byte{
|
||||||
testFile1: []byte("in manifest"),
|
testFile1: []byte("in manifest"),
|
||||||
}
|
}
|
||||||
createTestManifest(t, fs, testDataManifestPath, manifestFiles)
|
createTestManifest(t, fs, "/data/.index.mf", manifestFiles)
|
||||||
createFilesOnDisk(t, fs, map[string][]byte{
|
createFilesOnDisk(t, fs, map[string][]byte{
|
||||||
testFile1: []byte("in manifest"),
|
testFile1: []byte("in manifest"),
|
||||||
})
|
})
|
||||||
@@ -474,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: testDataManifestPath,
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results := make(chan Result, 10)
|
results := make(chan Result, 10)
|
||||||
@@ -493,7 +383,7 @@ func TestFindExtraFilesSkipsManifestAndDotfiles(t *testing.T) {
|
|||||||
extras = append(extras, r)
|
extras = append(extras, r)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should only report extra.txt, not .hidden, .config/settings, or index.mf
|
// Should only report extra.txt, not .hidden, .config/settings, or .index.mf
|
||||||
for _, e := range extras {
|
for _, e := range extras {
|
||||||
t.Logf("extra: %s", e.Path)
|
t.Logf("extra: %s", e.Path)
|
||||||
}
|
}
|
||||||
@@ -510,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())
|
||||||
@@ -533,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
|
||||||
@@ -553,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
|
||||||
@@ -577,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 {
|
||||||
@@ -587,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)
|
||||||
@@ -619,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)
|
||||||
@@ -658,7 +528,7 @@ func TestFindExtraFilesSkipsDotfiles(t *testing.T) {
|
|||||||
files := map[string][]byte{
|
files := map[string][]byte{
|
||||||
testFile1: []byte("in manifest"),
|
testFile1: []byte("in manifest"),
|
||||||
}
|
}
|
||||||
createTestManifest(t, fs, testDataManifestPath, files)
|
createTestManifest(t, fs, "/data/.index.mf", files)
|
||||||
createFilesOnDisk(t, fs, files)
|
createFilesOnDisk(t, fs, files)
|
||||||
|
|
||||||
// Add dotfiles and manifest file on disk
|
// Add dotfiles and manifest file on disk
|
||||||
@@ -667,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: testDataManifestPath,
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results := make(chan Result, 10)
|
results := make(chan Result, 10)
|
||||||
@@ -697,14 +563,10 @@ func TestFindExtraFilesSkipsManifestFile(t *testing.T) {
|
|||||||
files := map[string][]byte{
|
files := map[string][]byte{
|
||||||
testFile1: []byte("content"),
|
testFile1: []byte("content"),
|
||||||
}
|
}
|
||||||
createTestManifest(t, fs, testDataManifestPath, 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: testDataManifestPath,
|
|
||||||
BasePath: testDataDir,
|
|
||||||
Fs: fs,
|
|
||||||
})
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
results := make(chan Result, 10)
|
results := make(chan Result, 10)
|
||||||
@@ -725,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())
|
||||||
@@ -763,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
-22
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"io"
|
"io"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -58,8 +57,6 @@ type ScannerOptions struct {
|
|||||||
SigningOptions *SigningOptions
|
SigningOptions *SigningOptions
|
||||||
// Seed, if set, derives a deterministic UUID from this seed.
|
// Seed, if set, derives a deterministic UUID from this seed.
|
||||||
Seed string
|
Seed string
|
||||||
// ExcludePaths lists files to leave out of the listing, matched with os.SameFile.
|
|
||||||
ExcludePaths []string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileEntry represents a file that has been enumerated.
|
// FileEntry represents a file that has been enumerated.
|
||||||
@@ -78,7 +75,6 @@ type Scanner struct {
|
|||||||
totalBytes FileSize // cached sum of all file sizes
|
totalBytes FileSize // cached sum of all file sizes
|
||||||
options *ScannerOptions
|
options *ScannerOptions
|
||||||
fs afero.Fs
|
fs afero.Fs
|
||||||
excluded []fs.FileInfo // the files named in ExcludePaths that exist
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewScanner creates a new Scanner with default options.
|
// NewScanner creates a new Scanner with default options.
|
||||||
@@ -97,22 +93,11 @@ func NewScannerWithOptions(opts *ScannerOptions) *Scanner {
|
|||||||
fs = afero.NewOsFs()
|
fs = afero.NewOsFs()
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &Scanner{
|
return &Scanner{
|
||||||
files: make([]*FileEntry, 0),
|
files: make([]*FileEntry, 0),
|
||||||
options: opts,
|
options: opts,
|
||||||
fs: fs,
|
fs: fs,
|
||||||
}
|
}
|
||||||
|
|
||||||
// A path that cannot be stat'd, normally because no file is there,
|
|
||||||
// needs no leaving out.
|
|
||||||
for _, p := range opts.ExcludePaths {
|
|
||||||
info, err := s.fs.Stat(p)
|
|
||||||
if err == nil {
|
|
||||||
s.excluded = append(s.excluded, info)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EnumerateFile adds a single file to the scanner, calling stat() to get metadata.
|
// EnumerateFile adds a single file to the scanner, calling stat() to get metadata.
|
||||||
@@ -450,12 +435,6 @@ func (s *Scanner) enumerateFileWithInfo(
|
|||||||
info = realInfo
|
info = realInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, excluded := range s.excluded {
|
|
||||||
if os.SameFile(info, excluded) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
entry := &FileEntry{
|
entry := &FileEntry{
|
||||||
Path: RelFilePath(cleanPath),
|
Path: RelFilePath(cleanPath),
|
||||||
AbsPath: AbsFilePath(absPath),
|
AbsPath: AbsFilePath(absPath),
|
||||||
|
|||||||
@@ -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")
|
|
||||||
@@ -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 "$@"
|
|
||||||
@@ -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