Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
23dc6dd49d |
+2
-62
@@ -1,64 +1,4 @@
|
|||||||
# .dockerignore does NOT use .gitignore semantics. Docker matches with
|
|
||||||
# moby/patternmatcher: filepath.Match plus `**`, so `*` does not cross
|
|
||||||
# `/` and an unprefixed pattern is anchored at the context root. Every
|
|
||||||
# depth-independent pattern therefore needs `**/`, or `config/.env` and
|
|
||||||
# `certs/server.key` still ship while this file reads as solved. Only
|
|
||||||
# genuinely root-anchored entries go unprefixed. Never transplant these
|
|
||||||
# into .gitignore, where `**/` is wrong.
|
|
||||||
#
|
|
||||||
# Matching is case-sensitive, so secrets use character ranges rather
|
|
||||||
# than an ALL-CAPS twin, which would still miss `Server.Key`.
|
|
||||||
#
|
|
||||||
# Extend with this repo's own host-built artifacts, written anchored:
|
|
||||||
# `/myapp`, never `**/myapp`, which also matches `cmd/myapp/` and
|
|
||||||
# deletes the package directory from the context.
|
|
||||||
|
|
||||||
# .git is sent without its config. Without a VERSION build argument the
|
|
||||||
# stage that compiles runs `git describe --tags --always` on .git, which
|
|
||||||
# does not need .git/config; that file can hold a credential, such as a
|
|
||||||
# password in a remote URL or the token the CI checkout step stores there.
|
|
||||||
.git/config
|
|
||||||
|
|
||||||
# Agent scratch: one full checkout of the repo per in-flight agent.
|
|
||||||
# Anchored because it occurs once where agents run at the repo root.
|
|
||||||
# KNOWN GAP: a repo running agents in subdirectories still ships
|
|
||||||
# `services/api/.claude/` and must add its own anchored entry.
|
|
||||||
.claude
|
|
||||||
|
|
||||||
# Environment files. `*.env` covers bare `.env` and the `prod.env`
|
|
||||||
# convention. Re-include a committed template with a negation if the
|
|
||||||
# build needs one: `!docs/example.env`.
|
|
||||||
**/*.[eE][nN][vV]
|
|
||||||
**/.[eE][nN][vV].*
|
|
||||||
**/.[eE][nN][vV][rR][cC]
|
|
||||||
|
|
||||||
# Private keys and the bundles carrying them. Public certificates
|
|
||||||
# (*.crt, *.cer) are deliberately absent: they are legitimate inputs.
|
|
||||||
**/*.[pP][eE][mM]
|
|
||||||
**/*.[kK][eE][yY]
|
|
||||||
**/*.[pP]12
|
|
||||||
**/*.[pP][fF][xX]
|
|
||||||
**/[iI][dD]_[rR][sS][aA]
|
|
||||||
**/[iI][dD]_[dD][sS][aA]
|
|
||||||
**/[iI][dD]_[eE][cC][dD][sS][aA]
|
|
||||||
**/[iI][dD]_[eE][dD]25519
|
|
||||||
|
|
||||||
# Dependencies: restored inside the image, never copied in.
|
|
||||||
**/node_modules
|
|
||||||
|
|
||||||
# OS metadata.
|
|
||||||
**/.DS_Store
|
|
||||||
**/Thumbs.db
|
|
||||||
|
|
||||||
# Editor state: never a build input, and it churns COPY.
|
|
||||||
**/*.swp
|
|
||||||
**/*.swo
|
|
||||||
**/*~
|
|
||||||
**/*.bak
|
|
||||||
**/.idea
|
|
||||||
**/.vscode
|
|
||||||
**/*.sublime-*
|
|
||||||
|
|
||||||
# This repo's own host-built archives (Makefile).
|
|
||||||
*.tmp
|
*.tmp
|
||||||
*.dockerimage
|
*.dockerimage
|
||||||
|
.git
|
||||||
|
node_modules
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
root = true
|
|
||||||
|
|
||||||
[*]
|
|
||||||
indent_style = space
|
|
||||||
indent_size = 4
|
|
||||||
end_of_line = lf
|
|
||||||
charset = utf-8
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
insert_final_newline = true
|
|
||||||
|
|
||||||
[Makefile]
|
|
||||||
indent_style = tab
|
|
||||||
+2
-21
@@ -10,24 +10,5 @@ modcache.tzst
|
|||||||
# Generated manifest files
|
# Generated manifest files
|
||||||
.index.mf
|
.index.mf
|
||||||
|
|
||||||
# Secrets
|
# Stale files
|
||||||
.env
|
.drone.yml
|
||||||
.env.*
|
|
||||||
*.key
|
|
||||||
*.pem
|
|
||||||
|
|
||||||
# OS files
|
|
||||||
.DS_Store
|
|
||||||
Thumbs.db
|
|
||||||
|
|
||||||
# Editor files
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
|
|
||||||
# Go build artifacts
|
|
||||||
*.log
|
|
||||||
*.out
|
|
||||||
*.test
|
|
||||||
|
|||||||
+1
-20
@@ -48,26 +48,7 @@ COPY . .
|
|||||||
RUN touch mfer/mf.pb.go
|
RUN touch mfer/mf.pb.go
|
||||||
|
|
||||||
RUN make test
|
RUN make test
|
||||||
|
RUN cd cmd/mfer && go build -tags urfave_cli_no_docs -o /mfer .
|
||||||
# A build context sent as a tar archive, as upaas sends it, keeps its files'
|
|
||||||
# owners, and git refuses to read a checkout owned by another user.
|
|
||||||
RUN git config --system --add safe.directory /src
|
|
||||||
|
|
||||||
# The revision `mfer version` prints, stamped into main.Gitrev: the VERSION
|
|
||||||
# build argument when one is given (script/docker passes one), otherwise
|
|
||||||
# `git describe --tags --always` of the .git the build context carries: the
|
|
||||||
# tag on a tagged commit, tag-N-gHASH on a commit after one, the short commit
|
|
||||||
# when no tag is reachable. git ships in this base image. A context that
|
|
||||||
# carries .git and still yields no version fails the build.
|
|
||||||
ARG VERSION
|
|
||||||
RUN version="${VERSION:-$(git describe --tags --always)}"; \
|
|
||||||
if [ -e .git ] && { [ -z "$version" ] || [ "$version" = dev ] || \
|
|
||||||
[ "$version" = unknown ]; }; then \
|
|
||||||
echo "no version could be derived although the build context carries .git" >&2; \
|
|
||||||
exit 1; \
|
|
||||||
fi; \
|
|
||||||
cd cmd/mfer && \
|
|
||||||
go build -tags urfave_cli_no_docs -ldflags "-X main.Gitrev=$version" -o /mfer .
|
|
||||||
|
|
||||||
FROM scratch
|
FROM scratch
|
||||||
COPY --from=builder /mfer /mfer
|
COPY --from=builder /mfer /mfer
|
||||||
|
|||||||
@@ -13,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
|
.PHONY: bootstrap setup docker default run ci test check lint fmt fmt-check fmt-check-go fmt-check-md hooks fixme
|
||||||
|
|
||||||
default: fmt test
|
default: fmt test
|
||||||
|
|
||||||
@@ -32,9 +32,6 @@ ci: test
|
|||||||
test:
|
test:
|
||||||
@script/test
|
@script/test
|
||||||
|
|
||||||
fuzz:
|
|
||||||
@script/fuzz
|
|
||||||
|
|
||||||
$(PROTOC_GEN_GO):
|
$(PROTOC_GEN_GO):
|
||||||
test -e $(PROTOC_GEN_GO) || go install -v google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1
|
test -e $(PROTOC_GEN_GO) || go install -v google.golang.org/protobuf/cmd/protoc-gen-go@v1.28.1
|
||||||
|
|
||||||
|
|||||||
@@ -23,9 +23,8 @@ javascript library is planned.
|
|||||||
|
|
||||||
# Build Status
|
# Build Status
|
||||||
|
|
||||||
CI runs `script/cibuild`, which builds the Docker image with `--no-cache`, so
|
CI runs via `script/cibuild` (`docker build .`), which executes `make check`
|
||||||
the formatting, lint and test steps in the `Dockerfile` run on every build. The
|
(formatting, linting, tests). The `main` branch must always be green.
|
||||||
`main` branch must always be green.
|
|
||||||
|
|
||||||
# Entrypoints
|
# Entrypoints
|
||||||
|
|
||||||
@@ -44,9 +43,6 @@ provide:
|
|||||||
such as `script/docker`
|
such as `script/docker`
|
||||||
- `script/test` — run the test suite (`go test`), regenerating the protobuf code
|
- `script/test` — run the test suite (`go test`), regenerating the protobuf code
|
||||||
first if it is stale
|
first if it is stale
|
||||||
- `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
|
|
||||||
as ordinary tests
|
|
||||||
- `script/lint` — run `golangci-lint` and verify `gofmt` cleanliness
|
- `script/lint` — run `golangci-lint` and verify `gofmt` cleanliness
|
||||||
- `script/fmt` — format all code and docs (writes): `gofumpt`,
|
- `script/fmt` — format all code and docs (writes): `gofumpt`,
|
||||||
`golangci-lint run --fix`, and `script/prettier --write`
|
`golangci-lint run --fix`, and `script/prettier --write`
|
||||||
@@ -60,8 +56,8 @@ provide:
|
|||||||
Docker lint stage, whose image has no node
|
Docker lint stage, whose image has no node
|
||||||
- `script/check` — run `script/test`, `script/lint`, and `script/fmt-check`
|
- `script/check` — run `script/test`, `script/lint`, and `script/fmt-check`
|
||||||
- `script/docker` — build the Docker image tagged with the project name
|
- `script/docker` — build the Docker image tagged with the project name
|
||||||
- `script/cibuild` — CI entrypoint: builds the image with the same command as
|
- `script/cibuild` — CI entrypoint: `docker build .` (the Dockerfile runs the
|
||||||
`script/docker`, uncached, so the checks in the Dockerfile run every time
|
checks)
|
||||||
- `script/precommit` — pre-commit checks: `go mod tidy` verification, then
|
- `script/precommit` — pre-commit checks: `go mod tidy` verification, then
|
||||||
`script/check`
|
`script/check`
|
||||||
- `script/install-precommit` — install the git pre-commit hook that runs
|
- `script/install-precommit` — install the git pre-commit hook that runs
|
||||||
|
|||||||
@@ -24,34 +24,8 @@ only thing left of the `chore/align-repo-policies` branch is the list below.
|
|||||||
|
|
||||||
# Completed Steps
|
# Completed Steps
|
||||||
|
|
||||||
- 2026-10-03: added `FuzzNewManifestFromReader` and its seed corpus, which
|
- 2026-09-21: fixed the `-v` collision between `--verbose` and `--version`;
|
||||||
`make test` runs, plus `make fuzz` for a one-minute run by hand; the zstd
|
verbose owns `-v`, and version answers to `--version` and `-V` (#64)
|
||||||
decoder now has `MaxDecompressedSize` as its limit, so a frame claiming a
|
|
||||||
large size can no longer make the parser allocate that size (#65)
|
|
||||||
- 2026-10-03: `script/cibuild` builds the image with the same command as
|
|
||||||
`script/docker`, `--no-cache` included, so the checks in the Dockerfile run on
|
|
||||||
every build, also on an unchanged tree (#89)
|
|
||||||
- 2026-10-03: `fetch` removes whatever sits at a file's temp name and then
|
|
||||||
creates the temp file only if that name is free, so a hard link left there
|
|
||||||
cannot make it write into a file outside the destination directory (#115)
|
|
||||||
- 2026-10-03: `fetch` refuses any manifest path that runs through a symlink
|
|
||||||
already in the destination directory, checked before each of its writes
|
|
||||||
(directories, temp file, rename), so such a symlink cannot send a write
|
|
||||||
outside it (#86)
|
|
||||||
- 2026-10-02: a plain `docker build .` of a clone now stamps the tag or short
|
|
||||||
commit into `mfer version` instead of nothing: `.dockerignore` sends `.git`
|
|
||||||
(not `.git/config`), and the build stage takes the `VERSION` build argument,
|
|
||||||
otherwise `git describe --tags --always`, failing if `.git` is present and no
|
|
||||||
version comes out. `script/docker` is the canonical copy, which passes
|
|
||||||
`VERSION`; `bin/gitrev.sh` uses `--tags` too (#112)
|
|
||||||
- 2026-09-21: validate manifest entry paths on deserialize so untrusted `.mf`
|
|
||||||
files cannot make `Checker` stat or read outside `basePath` (#61)
|
|
||||||
- 2026-09-21: rewrote `script/test` to the canonical pattern (30s timeout,
|
|
||||||
`-race -cover`, quiet-first with verbose-on-failure rerun) and fixed the
|
|
||||||
process-global logger data race it surfaced (#67)
|
|
||||||
- 2026-09-21: added the canonical `.editorconfig`, made `.gitignore` cover
|
|
||||||
secrets, OS, editor, and Go artifacts, and removed the dead Drone CI
|
|
||||||
references from `.gitignore` and `bin/gitrev.sh` (#72)
|
|
||||||
- 2026-08-09: added `.prettierrc`/`.prettierignore`, gave `script/fmt` and
|
- 2026-08-09: added `.prettierrc`/`.prettierignore`, gave `script/fmt` and
|
||||||
`script/fmt-check` one shared prettier file set via `script/prettier`, dropped
|
`script/fmt-check` one shared prettier file set via `script/prettier`, dropped
|
||||||
the `|| true` that hid prettier failures, and added a node-based Dockerfile
|
the `|| true` that hid prettier failures, and added a node-based Dockerfile
|
||||||
@@ -90,6 +64,7 @@ only thing left of the `chore/align-repo-policies` branch is the list below.
|
|||||||
- Move FORMAT.md from repo root to docs/ and update the AGENTS.md reference
|
- Move FORMAT.md from repo root to docs/ and update the AGENTS.md reference
|
||||||
- Pin Makefile-installed Go tools (`protoc-gen-go@v1.28.1`,
|
- Pin Makefile-installed Go tools (`protoc-gen-go@v1.28.1`,
|
||||||
`golangci-lint@v2.12.2`) by module hash, not mutable tag
|
`golangci-lint@v2.12.2`) by module hash, not mutable tag
|
||||||
|
- Set `make test` timeout to 30s (currently 10s)
|
||||||
- Add explicit README "Rationale" heading (content exists under other
|
- Add explicit README "Rationale" heading (content exists under other
|
||||||
names); name the author in the README Description first line
|
names); name the author in the README Description first line
|
||||||
- Reconcile root-level AGENTS.md with directory-hygiene policy (keep or
|
- Reconcile root-level AGENTS.md with directory-hygiene policy (keep or
|
||||||
@@ -127,7 +102,7 @@ only thing left of the `chore/align-repo-policies` branch is the list below.
|
|||||||
rate-limit Checker progress output; add --deterministic flag or default;
|
rate-limit Checker progress output; add --deterministic flag or default;
|
||||||
wire top-level --version properly
|
wire top-level --version properly
|
||||||
- Testing:
|
- Testing:
|
||||||
- End-to-end tests for freshen and fetch
|
- Fuzz NewManifestFromReader; end-to-end tests for freshen and fetch
|
||||||
- Documentation:
|
- Documentation:
|
||||||
- Promote docs/FORMAT.md as primary spec reference; audit error messages;
|
- Promote docs/FORMAT.md as primary spec reference; audit error messages;
|
||||||
document the signature scheme fully
|
document the signature scheme fully
|
||||||
|
|||||||
+6
-1
@@ -1,7 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
#
|
#
|
||||||
|
if [[ ! -z "$DRONE_COMMIT_SHA" ]]; then
|
||||||
|
echo "${DRONE_COMMIT_SHA:0:7}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ ! -z "$GITREV" ]]; then
|
if [[ ! -z "$GITREV" ]]; then
|
||||||
echo $GITREV
|
echo $GITREV
|
||||||
else
|
else
|
||||||
git describe --tags --always --dirty=-dirty
|
git describe --always --dirty=-dirty
|
||||||
fi
|
fi
|
||||||
|
|||||||
+74
-12
@@ -27,6 +27,7 @@ const (
|
|||||||
testManifest = "/manifest.mf"
|
testManifest = "/manifest.mf"
|
||||||
testFlagBase = "--base"
|
testFlagBase = "--base"
|
||||||
testFlagNoExtra = "--no-extra-files"
|
testFlagNoExtra = "--no-extra-files"
|
||||||
|
testFlagVersion = "--version"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errSimulatedWrite = errors.New("simulated write failure")
|
var errSimulatedWrite = errors.New("simulated write failure")
|
||||||
@@ -102,7 +103,7 @@ func TestVersionCommand(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
opts := testOpts([]string{testApp, "version"}, fs)
|
opts := testOpts([]string{testApp, cmdVersion}, fs)
|
||||||
|
|
||||||
exitCode := runCLI(opts)
|
exitCode := runCLI(opts)
|
||||||
|
|
||||||
@@ -113,6 +114,69 @@ func TestVersionCommand(t *testing.T) {
|
|||||||
assert.Contains(t, stdout, "abc123")
|
assert.Contains(t, stdout, "abc123")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestVFlagCollision covers the -v/--verbose vs --version flag interaction
|
||||||
|
// (issue #64). Verbose owns -v; version answers to --version and -V. None of
|
||||||
|
// these invocations may produce a parser error, and the two ways of asking
|
||||||
|
// for the version must print the same thing.
|
||||||
|
func TestVFlagCollision(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Invocations that must print the version and exit 0.
|
||||||
|
versionCases := map[string][]string{
|
||||||
|
"long version flag": {testApp, testFlagVersion},
|
||||||
|
"short version flag": {testApp, "-V"},
|
||||||
|
"verbose then version": {testApp, "-v", testFlagVersion},
|
||||||
|
"long verbose and version": {testApp, "--verbose", testFlagVersion},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, args := range versionCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := testOpts(args, afero.NewMemMapFs())
|
||||||
|
exitCode := runCLI(opts)
|
||||||
|
|
||||||
|
assert.Equal(t, 0, exitCode, "stderr: %s", testStderr(t, opts))
|
||||||
|
assert.Contains(t, testStdout(t, opts), mfer.Version)
|
||||||
|
assert.NotContains(t, testStderr(t, opts), "two forms of the same flag")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invocations that must enable verbose and exit 0 without a parser error.
|
||||||
|
verboseCases := map[string][]string{
|
||||||
|
"short verbose flag": {testApp, "-v"},
|
||||||
|
"long verbose flag": {testApp, "--verbose"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, args := range verboseCases {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
opts := testOpts(args, afero.NewMemMapFs())
|
||||||
|
exitCode := runCLI(opts)
|
||||||
|
|
||||||
|
assert.Equal(t, 0, exitCode, "stderr: %s", testStderr(t, opts))
|
||||||
|
assert.Contains(t, testStdout(t, opts), cmdGenerate,
|
||||||
|
"root should show help listing subcommands")
|
||||||
|
assert.Empty(t, testStderr(t, opts))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestVersionFlagAndCommandMatch asserts that "mfer --version" and
|
||||||
|
// "mfer version" produce identical output (issue #64).
|
||||||
|
func TestVersionFlagAndCommandMatch(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
flagOpts := testOpts([]string{testApp, testFlagVersion}, afero.NewMemMapFs())
|
||||||
|
require.Equal(t, 0, runCLI(flagOpts))
|
||||||
|
|
||||||
|
cmdOpts := testOpts([]string{testApp, cmdVersion}, afero.NewMemMapFs())
|
||||||
|
require.Equal(t, 0, runCLI(cmdOpts))
|
||||||
|
|
||||||
|
assert.Equal(t, testStdout(t, flagOpts), testStdout(t, cmdOpts))
|
||||||
|
}
|
||||||
|
|
||||||
func TestHelpCommand(t *testing.T) {
|
func TestHelpCommand(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -126,7 +190,7 @@ func TestHelpCommand(t *testing.T) {
|
|||||||
stdout := testStdout(t, opts)
|
stdout := testStdout(t, opts)
|
||||||
assert.Contains(t, stdout, cmdGenerate)
|
assert.Contains(t, stdout, cmdGenerate)
|
||||||
assert.Contains(t, stdout, cmdCheck)
|
assert.Contains(t, stdout, cmdCheck)
|
||||||
assert.Contains(t, stdout, cmdFetch)
|
assert.Contains(t, stdout, "fetch")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGenerateCommand(t *testing.T) {
|
func TestGenerateCommand(t *testing.T) {
|
||||||
@@ -679,13 +743,11 @@ func TestCheckDetectsManifestCorruption(t *testing.T) {
|
|||||||
fs := afero.NewMemMapFs()
|
fs := afero.NewMemMapFs()
|
||||||
rng := rand.New(rand.NewSource(42)) //nolint:gosec // deterministic test data
|
rng := rand.New(rand.NewSource(42)) //nolint:gosec // deterministic test data
|
||||||
|
|
||||||
// Create many small files with random names so the manifest has many
|
// Create many small files with random names to generate a ~1MB manifest
|
||||||
// entries and random single-byte flips land at varied offsets. Each
|
// Each manifest entry is roughly 50-60 bytes, so we need ~20000 files
|
||||||
// manifest entry is roughly 50-60 bytes. Kept modest so the suite stays
|
|
||||||
// within its wall-clock budget under -race.
|
|
||||||
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
require.NoError(t, fs.MkdirAll(testDir, 0o755))
|
||||||
|
|
||||||
numFiles := 1500
|
numFiles := 20000
|
||||||
for range numFiles {
|
for range numFiles {
|
||||||
// Generate random filename
|
// Generate random filename
|
||||||
filename := fmt.Sprintf("/testdir/%08x%08x%08x.dat",
|
filename := fmt.Sprintf("/testdir/%08x%08x%08x.dat",
|
||||||
@@ -701,11 +763,11 @@ func TestCheckDetectsManifestCorruption(t *testing.T) {
|
|||||||
exitCode := runCLI(opts)
|
exitCode := runCLI(opts)
|
||||||
require.Equal(t, 0, exitCode, "generate should succeed")
|
require.Equal(t, 0, exitCode, "generate should succeed")
|
||||||
|
|
||||||
// Read the valid manifest and verify it has real size.
|
// Read the valid manifest and verify it's approximately 1MB
|
||||||
validManifest, err := afero.ReadFile(fs, testManifest)
|
validManifest, err := afero.ReadFile(fs, testManifest)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.GreaterOrEqual(t, len(validManifest), 64*1024,
|
require.GreaterOrEqual(t, len(validManifest), 1024*1024,
|
||||||
"manifest should be at least 64KB, got %d bytes", len(validManifest))
|
"manifest should be at least 1MB, got %d bytes", len(validManifest))
|
||||||
t.Logf("manifest size: %d bytes (%d files)", len(validManifest), numFiles)
|
t.Logf("manifest size: %d bytes (%d files)", len(validManifest), numFiles)
|
||||||
|
|
||||||
// First corruption: truncate the manifest
|
// First corruption: truncate the manifest
|
||||||
@@ -728,8 +790,8 @@ func TestCheckDetectsManifestCorruption(t *testing.T) {
|
|||||||
exitCode = runCLI(opts)
|
exitCode = runCLI(opts)
|
||||||
require.Equal(t, 0, exitCode, "check should pass with valid manifest")
|
require.Equal(t, 0, exitCode, "check should pass with valid manifest")
|
||||||
|
|
||||||
// Now do 100 random corruption iterations
|
// Now do 500 random corruption iterations
|
||||||
for i := range 100 {
|
for i := range 500 {
|
||||||
// Corrupt: write a random byte at a random offset
|
// Corrupt: write a random byte at a random offset
|
||||||
corrupted := make([]byte, len(validManifest))
|
corrupted := make([]byte, len(validManifest))
|
||||||
copy(corrupted, validManifest)
|
copy(corrupted, validManifest)
|
||||||
|
|||||||
+9
-66
@@ -36,11 +36,6 @@ const (
|
|||||||
// traversal bit for group and other must stay set.
|
// traversal bit for group and other must stay set.
|
||||||
dirPerms os.FileMode = 0o755
|
dirPerms os.FileMode = 0o755
|
||||||
|
|
||||||
// filePerms is the permission mode, before the umask, for downloaded
|
|
||||||
// files. It is the mode os.Create uses; like dirPerms, it keeps group
|
|
||||||
// and other read access.
|
|
||||||
filePerms os.FileMode = 0o666
|
|
||||||
|
|
||||||
// Bitrate unit thresholds in bits per second.
|
// Bitrate unit thresholds in bits per second.
|
||||||
bpsPerGbps = 1e9
|
bpsPerGbps = 1e9
|
||||||
bpsPerMbps = 1e6
|
bpsPerMbps = 1e6
|
||||||
@@ -58,9 +53,6 @@ var (
|
|||||||
// errPathTraversal indicates a manifest path escaping the target
|
// errPathTraversal indicates a manifest path escaping the target
|
||||||
// directory.
|
// directory.
|
||||||
errPathTraversal = errors.New("path traversal not allowed")
|
errPathTraversal = errors.New("path traversal not allowed")
|
||||||
// errSymlinkInPath indicates a manifest path running through a
|
|
||||||
// symlink that already exists in the target directory.
|
|
||||||
errSymlinkInPath = errors.New("symlink in path not allowed")
|
|
||||||
// errSizeMismatch indicates a downloaded file with an unexpected
|
// errSizeMismatch indicates a downloaded file with an unexpected
|
||||||
// size.
|
// size.
|
||||||
errSizeMismatch = errors.New("size mismatch")
|
errSizeMismatch = errors.New("size mismatch")
|
||||||
@@ -282,35 +274,6 @@ func sanitizePath(p string) (string, error) {
|
|||||||
return cleaned, nil
|
return cleaned, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// checkNoSymlinks returns an error if any part of the relative path p
|
|
||||||
// already exists as a symlink. sanitizePath checks p only as text, so
|
|
||||||
// without this a symlink inside the target directory could send a write
|
|
||||||
// to p outside of it. Parts that do not exist yet are fine: fetch creates
|
|
||||||
// them as plain directories and files. Call it immediately before each
|
|
||||||
// write: a symlink created after it returns is not caught.
|
|
||||||
func checkNoSymlinks(p string) error {
|
|
||||||
current := ""
|
|
||||||
|
|
||||||
for _, part := range strings.Split(p, string(filepath.Separator)) {
|
|
||||||
current = filepath.Join(current, part)
|
|
||||||
|
|
||||||
info, err := os.Lstat(current)
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to check %s for a symlink: %w", current, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if info.Mode()&os.ModeSymlink != 0 {
|
|
||||||
return fmt.Errorf("%w: %s", errSymlinkInPath, current)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, index.mf is appended.
|
// Otherwise, index.mf is appended.
|
||||||
@@ -456,12 +419,7 @@ func downloadFile(
|
|||||||
// Create parent directories if needed
|
// Create parent directories if needed
|
||||||
dir := filepath.Dir(localPath)
|
dir := filepath.Dir(localPath)
|
||||||
if dir != "" && dir != "." {
|
if dir != "" && dir != "." {
|
||||||
err = checkNoSymlinks(dir)
|
err := os.MkdirAll(dir, dirPerms)
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.MkdirAll(dir, dirPerms)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create directory %s: %w", dir, err)
|
return fmt.Errorf("failed to create directory %s: %w", dir, err)
|
||||||
}
|
}
|
||||||
@@ -489,25 +447,15 @@ func downloadFile(
|
|||||||
totalBytes = expectedSize
|
totalBytes = expectedSize
|
||||||
}
|
}
|
||||||
|
|
||||||
err = checkNoSymlinks(tmpPath)
|
// Create temp file.
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove whatever is at tmpPath, such as a leftover from an
|
|
||||||
// interrupted run, rather than write into it: it may be a hard link
|
|
||||||
// to a file outside the target directory, and removing a hard link
|
|
||||||
// removes only this name. If the removal fails, O_EXCL below makes
|
|
||||||
// the create fail.
|
|
||||||
_ = os.Remove(tmpPath)
|
|
||||||
|
|
||||||
// Create the temp file only if nothing is at tmpPath (O_EXCL).
|
|
||||||
//
|
//
|
||||||
// G304: tmpPath is a relative path that sanitizePath keeps inside the
|
// G304: tmpPath is derived from localPath, which sanitizePath above
|
||||||
// target directory as text, and checkNoSymlinks just found no symlink
|
// constrains lexically to a relative path that does not escape the
|
||||||
// in it.
|
// destination directory. That is a purely lexical guarantee: it does
|
||||||
out, err := os.OpenFile( //nolint:gosec // G304: see comment above
|
// not resolve symlinks, so a pre-existing symlink inside the
|
||||||
tmpPath, os.O_RDWR|os.O_CREATE|os.O_EXCL, filePerms)
|
// destination tree can still redirect this write outside of it
|
||||||
|
// (tracked in issue #86).
|
||||||
|
out, err := os.Create(tmpPath) //nolint:gosec // G304: see comment above
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create temp file: %w", err)
|
return fmt.Errorf("failed to create temp file: %w", err)
|
||||||
}
|
}
|
||||||
@@ -571,11 +519,6 @@ func finishDownload(
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = checkNoSymlinks(localPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rename temp file to final path
|
// Rename temp file to final path
|
||||||
err = os.Rename(tmpPath, localPath)
|
err = os.Rename(tmpPath, localPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -440,86 +440,3 @@ func TestFetchProgress(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, content, downloaded)
|
assert.Equal(t, content, downloaded)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestFetchRefusesSymlinks runs fetch into a destination directory that
|
|
||||||
// holds a symlink pointing outside it, in each of the three places fetch
|
|
||||||
// writes: a parent directory, the temp file, and the file itself, which
|
|
||||||
// the temp file is renamed onto; and once as a directory inside a plain
|
|
||||||
// directory. The fetch must fail and nothing outside may change.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest // changes the process-global working directory
|
|
||||||
func TestFetchRefusesSymlinks(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
entry string // the manifest's only file
|
|
||||||
link string // symlink placed in the destination directory
|
|
||||||
target string // what link points to, relative to the outside directory
|
|
||||||
}{
|
|
||||||
{"parent directory", "sub/deeper/file.txt", "sub", "."},
|
|
||||||
{"directory inside a plain directory", "docs/data/passwd", "docs/data", "."},
|
|
||||||
{"temp file", testFileTxt, ".file.txt.tmp", "new.txt"},
|
|
||||||
{"file", testFileTxt, testFileTxt, "new.txt"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
content := []byte("fetched")
|
|
||||||
sourceFs := afero.NewMemMapFs()
|
|
||||||
require.NoError(t, sourceFs.MkdirAll(filepath.Dir("/"+tt.entry), 0o755))
|
|
||||||
require.NoError(t, afero.WriteFile(sourceFs, "/"+tt.entry, content, 0o644))
|
|
||||||
|
|
||||||
server := httptest.NewServer(fetchTestHandler(
|
|
||||||
scanToManifest(t, sourceFs), map[string][]byte{tt.entry: content}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
outside := t.TempDir()
|
|
||||||
|
|
||||||
chdirTemp(t)
|
|
||||||
require.NoError(t, os.MkdirAll(filepath.Dir(tt.link), 0o750))
|
|
||||||
require.NoError(t, os.Symlink(filepath.Join(outside, tt.target), tt.link))
|
|
||||||
|
|
||||||
opts := testOpts([]string{testApp, cmdFetch, "-q", server.URL}, afero.NewOsFs())
|
|
||||||
assert.Equal(t, 1, runCLI(opts))
|
|
||||||
assert.Contains(t, testStderr(t, opts), "failed to download "+tt.entry+
|
|
||||||
": symlink in path not allowed: "+tt.link)
|
|
||||||
|
|
||||||
written, err := os.ReadDir(outside)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Empty(t, written, "fetch wrote outside the destination")
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestFetchReplacesHardLinkAtTempName runs fetch into a destination
|
|
||||||
// directory that holds, at the temp file's name, a hard link to a file
|
|
||||||
// outside it. To fetch that is an ordinary leftover from an interrupted
|
|
||||||
// earlier run: it must replace it and succeed, and the outside file must
|
|
||||||
// not change.
|
|
||||||
//
|
|
||||||
//nolint:paralleltest // changes the process-global working directory
|
|
||||||
func TestFetchReplacesHardLinkAtTempName(t *testing.T) {
|
|
||||||
content := []byte("fetched")
|
|
||||||
sourceFs := afero.NewMemMapFs()
|
|
||||||
require.NoError(t, afero.WriteFile(sourceFs, "/"+testFileTxt, content, 0o644))
|
|
||||||
|
|
||||||
server := httptest.NewServer(fetchTestHandler(
|
|
||||||
scanToManifest(t, sourceFs), map[string][]byte{testFileTxt: content}))
|
|
||||||
defer server.Close()
|
|
||||||
|
|
||||||
outsideFile := filepath.Join(t.TempDir(), "secret.txt")
|
|
||||||
require.NoError(t, os.WriteFile(outsideFile, []byte("outside"), 0o600))
|
|
||||||
|
|
||||||
chdirTemp(t)
|
|
||||||
require.NoError(t, os.Link(outsideFile, ".file.txt.tmp"))
|
|
||||||
|
|
||||||
opts := testOpts([]string{testApp, cmdFetch, "-q", server.URL}, afero.NewOsFs())
|
|
||||||
require.Equal(t, 0, runCLI(opts), testStderr(t, opts))
|
|
||||||
|
|
||||||
fetched, err := os.ReadFile(testFileTxt)
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, content, fetched)
|
|
||||||
|
|
||||||
outside, err := os.ReadFile(outsideFile) //nolint:gosec // test-controlled path
|
|
||||||
require.NoError(t, err)
|
|
||||||
assert.Equal(t, "outside", string(outside), "fetch wrote outside the destination")
|
|
||||||
}
|
|
||||||
|
|||||||
+27
-5
@@ -18,7 +18,7 @@ const (
|
|||||||
cmdGenerate = "generate"
|
cmdGenerate = "generate"
|
||||||
cmdCheck = "check"
|
cmdCheck = "check"
|
||||||
cmdExport = "export"
|
cmdExport = "export"
|
||||||
cmdFetch = "fetch"
|
cmdVersion = "version"
|
||||||
|
|
||||||
flagProgress = "progress"
|
flagProgress = "progress"
|
||||||
|
|
||||||
@@ -68,6 +68,12 @@ func (mfa *CLIApp) VersionString() string {
|
|||||||
return mfer.Version
|
return mfer.Version
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// printVersion writes the version line shared by the --version flag and the
|
||||||
|
// version subcommand, so both produce identical output.
|
||||||
|
func (mfa *CLIApp) printVersion() {
|
||||||
|
_, _ = fmt.Fprintf(mfa.Stdout, "%s version %s\n", mfa.appname, mfa.VersionString())
|
||||||
|
}
|
||||||
|
|
||||||
func (mfa *CLIApp) printBanner() {
|
func (mfa *CLIApp) printBanner() {
|
||||||
if log.GetLevel() <= log.InfoLevel {
|
if log.GetLevel() <= log.InfoLevel {
|
||||||
_, _ = fmt.Fprintln(mfa.Stdout, banner)
|
_, _ = fmt.Fprintln(mfa.Stdout, banner)
|
||||||
@@ -266,10 +272,10 @@ func (mfa *CLIApp) exportCommand() *cli.Command {
|
|||||||
|
|
||||||
func (mfa *CLIApp) versionCommand() *cli.Command {
|
func (mfa *CLIApp) versionCommand() *cli.Command {
|
||||||
return &cli.Command{
|
return &cli.Command{
|
||||||
Name: "version",
|
Name: cmdVersion,
|
||||||
Usage: "Show version",
|
Usage: "Show version",
|
||||||
Action: func(_ *cli.Context) error {
|
Action: func(_ *cli.Context) error {
|
||||||
_, _ = fmt.Fprintln(mfa.Stdout, mfa.VersionString())
|
mfa.printVersion()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@@ -301,7 +307,7 @@ 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: "fetch",
|
||||||
Usage: "fetch manifest and referenced files",
|
Usage: "fetch manifest and referenced files",
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
mfa.setVerbosity(c)
|
mfa.setVerbosity(c)
|
||||||
@@ -325,6 +331,20 @@ func (mfa *CLIApp) run(args []string) {
|
|||||||
log.SetOutput(mfa.Stdout, mfa.Stderr)
|
log.SetOutput(mfa.Stdout, mfa.Stderr)
|
||||||
log.Init()
|
log.Init()
|
||||||
|
|
||||||
|
// -v means verbose, not version. urfave/cli's built-in version flag
|
||||||
|
// claims -v by default, colliding with the -v verbose alias used here and
|
||||||
|
// on every subcommand; that collision makes "mfer -v --version" fail to
|
||||||
|
// parse. Verbose is the more common meaning of -v in tools that offer
|
||||||
|
// both, so verbose keeps -v and the version flag takes the capital -V.
|
||||||
|
// VersionFlag and VersionPrinter are urfave/cli package globals; run() is
|
||||||
|
// serialized in tests, so assigning them here is safe.
|
||||||
|
cli.VersionFlag = &cli.BoolFlag{
|
||||||
|
Name: cmdVersion,
|
||||||
|
Aliases: []string{"V"},
|
||||||
|
Usage: "print the version",
|
||||||
|
}
|
||||||
|
cli.VersionPrinter = func(_ *cli.Context) { mfa.printVersion() }
|
||||||
|
|
||||||
mfa.app = &cli.App{
|
mfa.app = &cli.App{
|
||||||
Name: mfa.appname,
|
Name: mfa.appname,
|
||||||
Usage: "Manifest generator",
|
Usage: "Manifest generator",
|
||||||
@@ -332,11 +352,13 @@ func (mfa *CLIApp) run(args []string) {
|
|||||||
EnableBashCompletion: true,
|
EnableBashCompletion: true,
|
||||||
Writer: mfa.Stdout,
|
Writer: mfa.Stdout,
|
||||||
ErrWriter: mfa.Stderr,
|
ErrWriter: mfa.Stderr,
|
||||||
|
Flags: commonFlags(),
|
||||||
Action: func(c *cli.Context) error {
|
Action: func(c *cli.Context) error {
|
||||||
if c.Args().Len() > 0 {
|
if c.Args().Len() > 0 {
|
||||||
return fmt.Errorf("%w %q", errUnknownCommand, c.Args().First())
|
return fmt.Errorf("%w %q", errUnknownCommand, c.Args().First())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mfa.setVerbosity(c)
|
||||||
mfa.printBanner()
|
mfa.printBanner()
|
||||||
|
|
||||||
return cli.ShowAppHelp(c)
|
return cli.ShowAppHelp(c)
|
||||||
@@ -358,6 +380,6 @@ func (mfa *CLIApp) run(args []string) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
mfa.exitCode = 1
|
mfa.exitCode = 1
|
||||||
|
|
||||||
log.Errorf("%s", err)
|
log.WithError(err).Debugf("exiting")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+41
-34
@@ -112,16 +112,13 @@ func DisableStyling() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Init initializes the logger with the CLI handler and default log level.
|
// Init initializes the logger with the CLI handler and default log level.
|
||||||
//
|
|
||||||
// It reconfigures the process-global apex/log logger under the write lock so
|
|
||||||
// the global is never mutated while another goroutine holds the read lock to
|
|
||||||
// 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.RLock()
|
||||||
defer mu.Unlock()
|
|
||||||
|
|
||||||
log.SetHandler(acli.New(stderr))
|
w := stderr
|
||||||
|
|
||||||
|
mu.RUnlock()
|
||||||
|
log.SetHandler(acli.New(w))
|
||||||
log.SetLevel(log.DebugLevel) // Let apex/log pass everything; we filter ourselves
|
log.SetLevel(log.DebugLevel) // Let apex/log pass everything; we filter ourselves
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,66 +130,74 @@ func isEnabled(l Level) bool {
|
|||||||
return l >= currentLevel
|
return l >= currentLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
// emit calls fn while holding the read lock if messages at level l are
|
|
||||||
// enabled. Holding the read lock across the apex/log call keeps the global
|
|
||||||
// logger from being read while Init reconfigures it under the write lock.
|
|
||||||
func emit(l Level, fn func()) {
|
|
||||||
mu.RLock()
|
|
||||||
defer mu.RUnlock()
|
|
||||||
|
|
||||||
if l >= currentLevel {
|
|
||||||
fn()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fatalf logs a formatted message at fatal level.
|
// Fatalf logs a formatted message at fatal level.
|
||||||
func Fatalf(format string, args ...any) {
|
func Fatalf(format string, args ...any) {
|
||||||
emit(FatalLevel, func() { log.Fatalf(format, args...) })
|
if isEnabled(FatalLevel) {
|
||||||
|
log.Fatalf(format, args...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fatal logs a message at fatal level.
|
// Fatal logs a message at fatal level.
|
||||||
func Fatal(arg string) {
|
func Fatal(arg string) {
|
||||||
emit(FatalLevel, func() { log.Fatal(arg) })
|
if isEnabled(FatalLevel) {
|
||||||
|
log.Fatal(arg)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
||||||
emit(ErrorLevel, func() { log.Errorf(format, args...) })
|
if isEnabled(ErrorLevel) {
|
||||||
|
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) {
|
||||||
emit(ErrorLevel, func() { log.Error(arg) })
|
if isEnabled(ErrorLevel) {
|
||||||
|
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) {
|
||||||
emit(WarnLevel, func() { log.Warnf(format, args...) })
|
if isEnabled(WarnLevel) {
|
||||||
|
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) {
|
||||||
emit(WarnLevel, func() { log.Warn(arg) })
|
if isEnabled(WarnLevel) {
|
||||||
|
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) {
|
||||||
emit(InfoLevel, func() { log.Infof(format, args...) })
|
if isEnabled(InfoLevel) {
|
||||||
|
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) {
|
||||||
emit(InfoLevel, func() { log.Info(arg) })
|
if isEnabled(InfoLevel) {
|
||||||
|
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) {
|
||||||
emit(VerboseLevel, func() { log.Infof(format, args...) })
|
if isEnabled(VerboseLevel) {
|
||||||
|
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) {
|
||||||
emit(VerboseLevel, func() { log.Info(arg) })
|
if isEnabled(VerboseLevel) {
|
||||||
|
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.
|
||||||
@@ -211,10 +216,7 @@ 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()
|
if !isEnabled(DebugLevel) {
|
||||||
defer mu.RUnlock()
|
|
||||||
|
|
||||||
if DebugLevel < currentLevel {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,6 +275,11 @@ func GetLevel() Level {
|
|||||||
return currentLevel
|
return currentLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithError returns a log entry with the error attached.
|
||||||
|
func WithError(e error) *log.Entry {
|
||||||
|
return log.Log.WithError(e)
|
||||||
|
}
|
||||||
|
|
||||||
// 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.
|
||||||
func Progressf(format string, args ...any) {
|
func Progressf(format string, args ...any) {
|
||||||
|
|||||||
@@ -312,10 +312,6 @@ func (c *Checker) FindExtraFiles(ctx context.Context, results chan<- Result) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *Checker) checkFile(entry *MFFilePath, checkedBytes *FileSize) Result {
|
func (c *Checker) checkFile(entry *MFFilePath, checkedBytes *FileSize) Result {
|
||||||
// entry.GetPath() is safe to join here: a manifest's entry paths are
|
|
||||||
// validated against the path invariants when it is loaded (see
|
|
||||||
// deserializeInner) or built (see Builder.AddFile), so a traversal or
|
|
||||||
// absolute path can never reach this point.
|
|
||||||
absPath := filepath.Join(string(c.basePath), entry.GetPath())
|
absPath := filepath.Join(string(c.basePath), entry.GetPath())
|
||||||
relPath := RelFilePath(entry.GetPath())
|
relPath := RelFilePath(entry.GetPath())
|
||||||
|
|
||||||
|
|||||||
+1
-20
@@ -25,7 +25,6 @@ var (
|
|||||||
errDecompressedTooLarge = errors.New("decompressed data exceeds maximum allowed size")
|
errDecompressedTooLarge = errors.New("decompressed data exceeds maximum allowed size")
|
||||||
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")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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).
|
||||||
@@ -111,12 +110,7 @@ func (m *manifest) verifyOuterIntegrity() error {
|
|||||||
func (m *manifest) decompressInner() ([]byte, error) {
|
func (m *manifest) decompressInner() ([]byte, error) {
|
||||||
bb := bytes.NewBuffer(m.pbOuter.GetInnerMessage())
|
bb := bytes.NewBuffer(m.pbOuter.GetInnerMessage())
|
||||||
|
|
||||||
// The decoder's own limit is 64 GiB. Up to that it sets aside as many
|
zr, err := zstd.NewReader(bb)
|
||||||
// bytes as a frame header claims, and it decodes a payload under
|
|
||||||
// 128 KiB in full before the LimitReader below reads any of it, so it
|
|
||||||
// needs MaxDecompressedSize as its limit too.
|
|
||||||
zr, err := zstd.NewReader(bb,
|
|
||||||
zstd.WithDecoderMaxMemory(uint64(MaxDecompressedSize)))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("deserialize: zstd reader: %w", err)
|
return nil, fmt.Errorf("deserialize: zstd reader: %w", err)
|
||||||
}
|
}
|
||||||
@@ -187,19 +181,6 @@ func (m *manifest) deserializeInner() error {
|
|||||||
return errUUIDMismatch
|
return errUUIDMismatch
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enforce the manifest path invariants on every entry as it is loaded,
|
|
||||||
// so that no consumer of a manifest — Checker today, any restore or
|
|
||||||
// extract path tomorrow — acts on a traversal or absolute path from an
|
|
||||||
// untrusted .mf. Reject loudly on the first offender rather than
|
|
||||||
// dropping entries, which would let a hostile manifest hide files from a
|
|
||||||
// check.
|
|
||||||
for _, f := range m.pbInner.GetFiles() {
|
|
||||||
err = ValidatePath(f.GetPath())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: %w", errInvalidManifestPath, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("loaded manifest with %d files", len(m.pbInner.GetFiles()))
|
log.Infof("loaded manifest with %d files", len(m.pbInner.GetFiles()))
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
//nolint:testpackage // white-box tests exercise unexported internals
|
|
||||||
package mfer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"runtime"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// FuzzNewManifestFromReader feeds arbitrary bytes to the manifest parser.
|
|
||||||
// `make test` runs it on the seed corpus in
|
|
||||||
// testdata/fuzz/FuzzNewManifestFromReader; `make fuzz` searches for new
|
|
||||||
// inputs.
|
|
||||||
//
|
|
||||||
// For every input the parser must return a manifest or an error, not both
|
|
||||||
// and not neither, and must not allocate more than a fixed multiple of its
|
|
||||||
// input plus MaxDecompressedSize. A panic or a hang fails the test on its
|
|
||||||
// own.
|
|
||||||
func FuzzNewManifestFromReader(f *testing.F) {
|
|
||||||
// A signed manifest makes the parser write the key and signature to a
|
|
||||||
// temporary directory and run gpg on them. With gpg off the PATH and
|
|
||||||
// temporary files kept in the test's own directory, no process is
|
|
||||||
// started and nothing is written elsewhere; such input ends in an
|
|
||||||
// error instead.
|
|
||||||
f.Setenv("PATH", "")
|
|
||||||
f.Setenv("TMPDIR", f.TempDir())
|
|
||||||
|
|
||||||
f.Fuzz(func(t *testing.T, data []byte) {
|
|
||||||
var before, after runtime.MemStats
|
|
||||||
|
|
||||||
runtime.ReadMemStats(&before)
|
|
||||||
|
|
||||||
m, err := NewManifestFromReader(bytes.NewReader(data))
|
|
||||||
|
|
||||||
runtime.ReadMemStats(&after)
|
|
||||||
|
|
||||||
if (m == nil) == (err == nil) {
|
|
||||||
t.Fatalf("got manifest %p and error %v, want exactly one", m, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The parser keeps a copy of its input and at most
|
|
||||||
// MaxDecompressedSize bytes of decompressed data. Buffers grow by
|
|
||||||
// copying, so reaching those sizes allocates a few times them in
|
|
||||||
// total. Sixteen times leaves room for that, and none for a
|
|
||||||
// decoder that sets aside whatever size its input claims.
|
|
||||||
limit := 16 * (uint64(len(data)) + uint64(MaxDecompressedSize))
|
|
||||||
|
|
||||||
allocated := after.TotalAlloc - before.TotalAlloc
|
|
||||||
if allocated > limit {
|
|
||||||
t.Fatalf("allocated %d bytes for %d bytes of input, limit %d",
|
|
||||||
allocated, len(data), limit)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
//nolint:testpackage // white-box tests exercise unexported internals
|
|
||||||
package mfer
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/google/uuid"
|
|
||||||
"github.com/klauspost/compress/zstd"
|
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
"google.golang.org/protobuf/encoding/protowire"
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// craftInnerBytes builds the wire bytes of an inner MFFile holding a single
|
|
||||||
// file entry whose path is exactly pathBytes. It writes the wire form by hand
|
|
||||||
// so a hostile path — including one that is not valid UTF-8 — can be embedded
|
|
||||||
// without proto.Marshal's own UTF-8 enforcement rejecting it first.
|
|
||||||
func craftInnerBytes(id uuid.UUID, pathBytes string) []byte {
|
|
||||||
entry := protowire.AppendTag(nil, 1, protowire.BytesType) // MFFilePath.path
|
|
||||||
entry = protowire.AppendString(entry, pathBytes)
|
|
||||||
|
|
||||||
inner := protowire.AppendTag(nil, 100, protowire.VarintType) // MFFile.version
|
|
||||||
inner = protowire.AppendVarint(inner, uint64(MFFile_VERSION_ONE))
|
|
||||||
inner = protowire.AppendTag(inner, 101, protowire.BytesType) // MFFile.files
|
|
||||||
inner = protowire.AppendBytes(inner, entry)
|
|
||||||
inner = protowire.AppendTag(inner, 102, protowire.BytesType) // MFFile.uuid
|
|
||||||
inner = protowire.AppendBytes(inner, id[:])
|
|
||||||
|
|
||||||
return inner
|
|
||||||
}
|
|
||||||
|
|
||||||
// wrapInner wraps inner MFFile wire bytes in a complete, well-formed .mf
|
|
||||||
// envelope (magic prefix, zstd-compressed payload, matching hash and UUID) so
|
|
||||||
// that deserialization reaches path validation rather than failing earlier on
|
|
||||||
// an integrity check.
|
|
||||||
func wrapInner(t *testing.T, id uuid.UUID, innerData []byte) []byte {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
var cbuf bytes.Buffer
|
|
||||||
|
|
||||||
zw, err := zstd.NewWriter(&cbuf, zstd.WithEncoderLevel(zstd.SpeedBestCompression))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
_, err = zw.Write(innerData)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NoError(t, zw.Close())
|
|
||||||
|
|
||||||
compressed := cbuf.Bytes()
|
|
||||||
sum := sha256.Sum256(compressed)
|
|
||||||
|
|
||||||
outer := &MFFileOuter{
|
|
||||||
InnerMessage: compressed,
|
|
||||||
Size: int64(len(innerData)),
|
|
||||||
Sha256: sum[:],
|
|
||||||
Uuid: id[:],
|
|
||||||
Version: MFFileOuter_VERSION_ONE,
|
|
||||||
CompressionType: MFFileOuter_COMPRESSION_ZSTD,
|
|
||||||
}
|
|
||||||
|
|
||||||
ob, err := proto.Marshal(outer)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
return append([]byte(MAGIC), ob...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeserializeRejectsInvalidEntryPaths(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
path string
|
|
||||||
}{
|
|
||||||
{"parent traversal", "../escape"},
|
|
||||||
{"interior traversal", "a/../../escape"},
|
|
||||||
{"absolute path", "/etc/passwd"},
|
|
||||||
{"backslash path", `a\b`},
|
|
||||||
{"double slash", "a//b"},
|
|
||||||
{"empty path", ""},
|
|
||||||
{"invalid utf-8", "abc\xff"},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
id := uuid.New()
|
|
||||||
data := wrapInner(t, id, craftInnerBytes(id, tt.path))
|
|
||||||
|
|
||||||
_, err := NewManifestFromReader(bytes.NewReader(data))
|
|
||||||
require.Error(t, err)
|
|
||||||
|
|
||||||
if tt.path == "abc\xff" {
|
|
||||||
// A path that is not valid UTF-8 cannot survive the proto3
|
|
||||||
// string decoder, which rejects it before path validation
|
|
||||||
// runs; the manifest is still refused at load time.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
require.ErrorIs(t, err, errInvalidManifestPath)
|
|
||||||
|
|
||||||
if tt.path != "" {
|
|
||||||
// ValidatePath quotes the path with %q; assert against the
|
|
||||||
// same rendering so escaped characters (e.g. a backslash)
|
|
||||||
// still match.
|
|
||||||
assert.Contains(t, err.Error(), fmt.Sprintf("%q", tt.path),
|
|
||||||
"error must name the offending path")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeserializeValidManifestRoundTrips(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
hash := make([]byte, 34) // multihash: 2-byte prefix + 32-byte SHA-256
|
|
||||||
|
|
||||||
b := NewBuilder()
|
|
||||||
require.NoError(t, b.AddFileWithHash("dir/file.txt", 123, ModTime{}, hash))
|
|
||||||
|
|
||||||
var buf bytes.Buffer
|
|
||||||
require.NoError(t, b.Build(&buf))
|
|
||||||
|
|
||||||
m, err := NewManifestFromReader(bytes.NewReader(buf.Bytes()))
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
files := m.Files()
|
|
||||||
require.Len(t, files, 1)
|
|
||||||
assert.Equal(t, "dir/file.txt", files[0].GetPath())
|
|
||||||
assert.Equal(t, int64(123), files[0].GetSize())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestValidatePathRejectsInvalidUTF8 pins the ValidatePath rule that a manifest
|
|
||||||
// path must be valid UTF-8, independent of the proto decoder that also enforces
|
|
||||||
// it on the wire.
|
|
||||||
func TestValidatePathRejectsInvalidUTF8(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
err := ValidatePath("abc\xff")
|
|
||||||
require.ErrorIs(t, err, errPathNotUTF8)
|
|
||||||
assert.Contains(t, err.Error(), "UTF-8")
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
[]byte("")
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
[]byte("ZNAVSRFGy[i\x04\xe5O\x82A\x1d\xf4\xb0\xe2z7:U\xee\xa3\xf9\xd6m\xacZ\x9b\xce\x1d\xd9/{@\x1d\xa5y[i\x04\xe5O\x82A\x1d\xf4\xb0\xe2z7:U\xee\xa3\xf9\xd6m\xacZ\x9b\xce\x1d\xd9/{@\x1d\xa5")
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
[]byte("ZNAVSRFG\xa8\x06\x01\xb0\x06\x01\xb8\x06V\xc2\x06 \xa3\xf7\x97\xa7\xf3\x87:\x90)\\ӊj\xb9\xf7\xfaTJ\x1b\xe7:\xee\xbe\"V\xe0:\x8d(Z\xd6%\xca\x06\x10\x93\x85\vpu\x85\xe4\x04\xe4\x95\x1a=\xdc\x1f\x05\xa3\xba\fc(\xb5/\xfd\x04\x00\xb1\x02\x00\xa0\x06\x01\xaa\x06=\n\x05a.txt\x10\x01\x1a$\n\"\x12 ʗ\x81\x12\xca\x1b\xbd\xca\xfa\xc21\xb3\x9a#\xdcM\xa7\x86\xef\xf8\x14|Nr\xb9\x80w\x85\xaf\xeeH\xbb\xf2\x12\v\b\x80\x92\xb8Ø\xfe\xff\xff\xff\x01\xb2\x06\x10\x93\x85\vpu\x85\xe4\x04\xe4\x95\x1a=\xdc\x1f\x05\xa3s\xeeG\x80")
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
[]byte("ZNAVSRFG\xa8\x06\x01\xb0\x06\x01\xb8\x06V\xc2\x06 \xa3\xf7\x97\xa7\xf3\x87:\x90)\\ӊj\xb9\xf7\xfaTJ\x1b\xe7:\xee\xbe\"V\xe0:\x8d(Z\xd6%\xca\x06\x10\x93\x85\vpu\x85\xe4\x04\xe4\x95\x1a=\xdc\x1f\x05\xa3\xba\fc(\xb5/\xfd\x04\x00\xb1\x02\x00\xa0\x06\x01\xaa\x06=\n\x05a.txt\x10\x01\x1a$\n\"\x12 ʗ\x81\x12\xca\x1b\xbd\xca\xfa\xc21\xb3\x9a#\xdcM\xa7\x86\xef\xf8\x14|Nr\xb9\x80w\x85\xaf\xeeH\xbb\xf2\x12\v\b\x80\x92\xb8Ø\xfe\xff\xff\xff\x01\xb2\x06\x10\x93\x85\vpu\x85\xe4\x04\xe4\x95\x1a=\xdc\x1f\x05\xa3s\xeeG\x80\xca\f\xe8\x03-----BEGIN PGP SIGNATURE-----\n\niQEzBAABCgAdFiEET1Yr+4Y/3GtRtO6IhypRF2zvI64FAmrBIXAACgkQhypRF2zv\nI67BQAf/QrpX2MjY15YGMGkjR5oIhnx/YV96aGYZyZThzb+l/R/N75iVFVkhX21d\nZhQqdCsORrodTPAXic2g2UGVXP9PhNMh7n6Wm3LsvQYjrRQGrQnqtCkut+3tUt8K\n7pt4OAnnwRSieaVImA1COmzxIrQQKNOs6UkgmAstGuPV0XZoeDiSG8TUYJ/vieCn\np5hC0FFXtzfw4NtkxSmkewE0xBxIwFCA/RfSHCGH3m5K+tRz41vMEgGbL1iEp6+V\nuBaoEc4hqCgEt+Af2pA8VHfqeu2vKiwggOpYpaILXZKVqH9+tWHL1EBv9t0vTsYE\n9D57euuR9+kOdngYNPieP1yn5dOSHg==\n=kvgL\n-----END PGP SIGNATURE-----\n\xd2\f(4F562BFB863FDC6B51B4EE88872A51176CEF23AE\xda\f\xb5\a-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nmQENBGrBIW8BCADESetN5EdxIe7Fafgxl99Yoo5cOexf7wJyYT0wfUYlRaxt3neR\nhir7LOfH4PZWWoDx7qghxCS4+vs7yGypl6JOm7jnJlhn4HneDa2zeIlgGW2TamyE\nua9KPWBQqkFOYmKPmzp+KnL6ncnBLR5mDkNKFyON812KVvteu6Dp/DNk4Meufe44\nWWr49LSFZa9gEbmRCoQGKby9F0H0yIi4FAc74VdQudy0+fMKcfkKjEvByMzlbBEK\n92Hq3sRFzWd3kvPliNjZTmlh5n5m9aBhMpoy3GkKy8gpDdFc6NLA9iAJe7oNMriR\nkVoa5EjQL1xCXAiAWTYA9NScFfU/574sCTxZABEBAAG0Hk1GRVIgVGVzdCBLZXkg\nPHRlc3RAbWZlci50ZXN0PokBTwQTAQoAORYhBE9WK/uGP9xrUbTuiIcqURds7yOu\nBQJqwSFvAxsvBAULCQgHAgYVCgkICwIEFgIDAQIeAQIXgAAKCRCHKlEXbO8jrjml\nCAC8wUK9wmvxq0+NZUpFyP+P29klLZYzBDaBrLPJFs0GjnG4kvfUAktWx0Ro80F7\ncjTJ4f44XjDj4glvSjbe2VaDnZl9FTfzUfG+xjD4462NgntQ4fHk/uG4F6d1ikWx\nkEoMpIn1PlSMas1jTQSGlxUr+zFwWuUbGq4n6hRxEnwLlwJlwQt/Aw1vPDYuPDE3\nOYDhJIAJyP+6e9W8ToaAG9byg/22KA1u1qxnNQqsx5Tped2VltAzdYub+yeCuNc8\nIUo5ILo/fQq3GM5sUEaHjPolv88WlDm3vcdbSbDoh5m2inDtg5zuUKJwu32UGu8l\nKoTjp4nxgQGy5WmeBzs4Hdm/\n=TCB8\n-----END PGP PUBLIC KEY BLOCK-----\n")
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
[]byte("ZNAVSRFG\xa8\x06\x01\xb0\x06\x01\xb8\x06!\xc2\x06 \x91\x90*\xa5>\fݐ \x87\xbeaL\xc1\x05?\x0eR\xc18\xa4eՕ\xa95\xb9KʺoZ\xca\x06\x10\x93\x85\vpu\x85\xe4\x04\xe4\x95\x1a=\xdc\x1f\x05\xa3\xba\f-(\xb5/\xfd\x04\x00\x01\x01\x00\xa0\x06\x01\xaa\x06\a\n\x05a.txt\xb2\x06\x10\x93\x85\vpu\x85\xe4\x04\xe4\x95\x1a=\xdc\x1f\x05\xa3a[k'")
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
[]byte("ZNAVSRFG\xa8\x06\x01\xb0\x06\x01\xb8\x06\x1f\xc2\x06 \x91\x90*\xa5>\fݐ \x87\xbeaL\xc1\x05?\x0eR\xc18\xa4eՕ\xa95\xb9KʺoZ\xca\x06\x10\x93\x85\vpu\x85\xe4\x04\xe4\x95\x1a=\xdc\x1f\x05\xa3\xba\f-(\xb5/\xfd\x04\x00\x01\x01\x00\xa0\x06\x01\xaa\x06\a\n\x05a.txt\xb2\x06\x10\x93\x85\vpu\x85\xe4\x04\xe4\x95\x1a=\xdc\x1f\x05\xa3a[k'")
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
[]byte("ZNAVSRFG\xa8\x06\x01\xb0\x06\x01\xb8\x06V\xc2\x06 \xa3\xf7\x97\xa7\xf3\x87:\x90)\\ӊj\xb9\xf7\xfaTJ\x1b\xe7:\xee\xbe\"V\xe0:\x8d(Z\xd6%\xca\x06\x10\x93\x85\vpu\x85\xe4\x04\xe4\x95\x1a=\xdc\x1f\x05\xa3\xba\fc(\xb5/\xfd\x04\x00\xb1\x02\x00\xa0\x06\x01\xaa\x06=\n\x05a.txt\x10\x01\x1a$\n\"\x12 ʗ\x81\x12\xca\x1b\xbd\xca\xfa\xc21\xb3\x9a#\xdcM\xa7\x86\xef\xf8\x14|Nr\xb9\x80w\x85\xaf\xeeH\xbb\xf2\x12\v\b\x80\x92\xb8Ø\xfe\xff\xff\xff\x01\xb2\x06\x10\x93\x85\vpu\x85\xe4\x04\xe4\x95\x1a=\xdc\x1f\x05\xa3s\xeeG")
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
[]byte("ZNAVSRFG\xa8\x06\x01\xb0\x06\x01\xb8\x06V\xc2\x06 ")
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
[]byte("ZNAV")
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
[]byte("ZNAVSRFG")
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
[]byte("ZNAVSRFG\xa8\x06\x01\xb0\x06\x01\xb8\x06V\xc2\x06 \xa3\xf7\x97\xa7\xf3\x87:\x90)\\ӊj\xb9\xf7\xfaTJ\x1b\xe7:\xee\xbe\"V\xe0:\x8d(Z\xd6%\xca\x06\x10\x93\x85\vpu\x85\xe4\x04\xe4\x95\x1a=\xdc\x1f\x05\xa3\xba\fc(\xb5/\xfd\x04\x00\xb1\x02\x00\xa0\x06\x01")
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
[]byte("ZNAVSRFX\xa8\x06\x01\xb0\x06\x01\xb8\x06V\xc2\x06 \xa3\xf7\x97\xa7\xf3\x87:\x90)\\ӊj\xb9\xf7\xfaTJ\x1b\xe7:\xee\xbe\"V\xe0:\x8d(Z\xd6%\xca\x06\x10\x93\x85\vpu\x85\xe4\x04\xe4\x95\x1a=\xdc\x1f\x05\xa3\xba\fc(\xb5/\xfd\x04\x00\xb1\x02\x00\xa0\x06\x01\xaa\x06=\n\x05a.txt\x10\x01\x1a$\n\"\x12 ʗ\x81\x12\xca\x1b\xbd\xca\xfa\xc21\xb3\x9a#\xdcM\xa7\x86\xef\xf8\x14|Nr\xb9\x80w\x85\xaf\xeeH\xbb\xf2\x12\v\b\x80\x92\xb8Ø\xfe\xff\xff\xff\x01\xb2\x06\x10\x93\x85\vpu\x85\xe4\x04\xe4\x95\x1a=\xdc\x1f\x05\xa3s\xeeG\x80")
|
|
||||||
File diff suppressed because one or more lines are too long
@@ -1,2 +0,0 @@
|
|||||||
go test fuzz v1
|
|
||||||
[]byte("ZNAVSRFG\xa8\x06\x01\xb0\x06\x01\xb8\x06\x01\xc2\x06 \xd6aQ\xa4\x85\xc0+\xbb\xca\x11\x11\a<\x019\x97\xb3\xbb3\xd0 \xd5U\xfa!\xaeAf<N@\x9d\xca\x06\x10\x93\x85\vpu\x85\xe4\x04\xe4\x95\x1a=\xdc\x1f\x05\xa3\xba\f\x12(\xb5/\xfd\xc0\x00\x00\x00\x00\x00\x02\x00\x00\x00\v\x00\x00\x00")
|
|
||||||
+5
-15
@@ -1,24 +1,14 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# script/cibuild: run the CI build; the Gitea workflow runs this on push.
|
# script/cibuild: run the CI build. The Dockerfile runs script/check
|
||||||
# It builds the image with the same command as script/docker. --no-cache
|
# (via make check), so a successful build implies all checks pass.
|
||||||
# because the checks the final stage depends on are RUN steps, and a
|
# Generic: needs no adaptation. The Gitea workflow runs this on push.
|
||||||
# cached one is a check that did not run.
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
|
||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
# Own line: a failing command substitution inside an argument does
|
docker build .
|
||||||
# not trip `set -e`, so the inline form degrades silently to an
|
|
||||||
# empty constant. The VERSION build argument takes precedence over
|
|
||||||
# the version a build stage derives from the .git in the context.
|
|
||||||
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
|
||||||
[ -n "$version" ] || version="unknown"
|
|
||||||
docker build --no-cache \
|
|
||||||
--build-arg VERSION="$version" \
|
|
||||||
-t "$("$SCRIPT_DIR/projectname")" .
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
+2
-11
@@ -1,8 +1,7 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# script/docker: build the Docker image tagged with the project name.
|
# script/docker: build the Docker image tagged with the project name.
|
||||||
# Identical in all repos; the tag comes from script/projectname.
|
# Identical in all repos; the tag comes from script/projectname.
|
||||||
# --no-cache because the gate phases the final stage depends on are RUN
|
# Generic: needs no adaptation.
|
||||||
# steps, and a cached one is a check that did not run.
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||||
@@ -10,15 +9,7 @@ ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
|||||||
|
|
||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
# Own line: a failing command substitution inside an argument does
|
docker build -t "$("$SCRIPT_DIR/projectname")" .
|
||||||
# not trip `set -e`, so the inline form degrades silently to an
|
|
||||||
# empty constant. The VERSION build argument takes precedence over
|
|
||||||
# the version a build stage derives from the .git in the context.
|
|
||||||
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
|
||||||
[ -n "$version" ] || version="unknown"
|
|
||||||
docker build --no-cache \
|
|
||||||
--build-arg VERSION="$version" \
|
|
||||||
-t "$("$SCRIPT_DIR/projectname")" .
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
-17
@@ -1,17 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
# script/fuzz: fuzz the manifest parser for one minute. Run by hand only:
|
|
||||||
# script/test already runs the committed seed corpus as ordinary tests,
|
|
||||||
# and CI never fuzzes. An input that fails is written to
|
|
||||||
# mfer/testdata/fuzz/FuzzNewManifestFromReader/; once the parser is fixed,
|
|
||||||
# commit it there as a regression seed.
|
|
||||||
set -eu
|
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
|
||||||
|
|
||||||
main() {
|
|
||||||
cd "$ROOT"
|
|
||||||
go test -run '^$' -fuzz '^FuzzNewManifestFromReader$' \
|
|
||||||
-fuzztime 1m -parallel 2 ./mfer
|
|
||||||
}
|
|
||||||
|
|
||||||
main "$@"
|
|
||||||
+1
-6
@@ -17,12 +17,7 @@ ensure_pb() {
|
|||||||
main() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
ensure_pb
|
ensure_pb
|
||||||
go test -timeout 30s -race -cover ./... ||
|
go test -v --timeout 10s ./...
|
||||||
{
|
|
||||||
echo "--- Rerunning with -v for details ---"
|
|
||||||
go test -timeout 30s -race -v ./...
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
main "$@"
|
main "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user