Compare commits
11 Commits
main
...
386b22efb8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
386b22efb8 | ||
|
|
77de489063 | ||
|
|
211f7e6f61 | ||
|
|
f68281d1ce | ||
|
|
655dfee585 | ||
|
|
da23fb774b | ||
|
|
2424be9bc6 | ||
|
|
a368d431f2 | ||
|
|
333dc8059c | ||
|
|
41c1c69f52 | ||
|
|
c8381792cf |
@@ -1,9 +0,0 @@
|
|||||||
name: check
|
|
||||||
on: [push]
|
|
||||||
jobs:
|
|
||||||
check:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
# actions/checkout v4.2.2, 2026-03-16
|
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
|
||||||
- run: docker build .
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
@@ -3,11 +3,6 @@
|
|||||||
*.tmp
|
*.tmp
|
||||||
*.dockerimage
|
*.dockerimage
|
||||||
/vendor
|
/vendor
|
||||||
vendor.tzst
|
|
||||||
modcache.tzst
|
|
||||||
|
|
||||||
# Generated manifest files
|
|
||||||
.index.mf
|
|
||||||
|
|
||||||
# Stale files
|
# Stale files
|
||||||
.drone.yml
|
.drone.yml
|
||||||
|
|||||||
29
AGENTS.md
29
AGENTS.md
@@ -1,29 +0,0 @@
|
|||||||
# Agent Instructions
|
|
||||||
|
|
||||||
Read `REPO_POLICIES.md` before making any changes. It is the authoritative
|
|
||||||
source for coding standards, formatting, linting, and workflow rules.
|
|
||||||
|
|
||||||
## Workflow
|
|
||||||
|
|
||||||
- When fixing a bug, write a failing test FIRST. Only after the test fails,
|
|
||||||
write the code to fix the bug. Then ensure the test passes. Leave the test in
|
|
||||||
place and commit it with the bugfix. Don't run shell commands to test bugfixes
|
|
||||||
or reproduce bugs. Write tests!
|
|
||||||
|
|
||||||
- After each change, run `make fmt`, then `make test`, then `make lint`. Fix any
|
|
||||||
failures before committing.
|
|
||||||
|
|
||||||
- After each change, commit only the files you've changed. Push after committing.
|
|
||||||
|
|
||||||
## Attribution
|
|
||||||
|
|
||||||
- Never mention Claude, Anthropic, or any AI/LLM tooling in commit messages. Do
|
|
||||||
not use attribution.
|
|
||||||
|
|
||||||
## Repository-Specific Notes
|
|
||||||
|
|
||||||
- This is a Go library + CLI tool for generating `.mf` manifest files.
|
|
||||||
- The proto definition is in `mfer/mf.proto`; generated `.pb.go` files are
|
|
||||||
committed (required for `go get` compatibility).
|
|
||||||
- The format specification is in `FORMAT.md`.
|
|
||||||
- See `TODO.md` for the 1.0 implementation plan and open design questions.
|
|
||||||
20
CLAUDE.md
Normal file
20
CLAUDE.md
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
# Important Rules
|
||||||
|
|
||||||
|
- when fixing a bug, write a failing test FIRST. only after the test fails, write
|
||||||
|
the code to fix the bug. then ensure the test passes. leave the test in
|
||||||
|
place and commit it with the bugfix. don't run shell commands to test
|
||||||
|
bugfixes or reproduce bugs. write tests!
|
||||||
|
|
||||||
|
- never, ever mention claude or anthropic in commit messages. do not use attribution
|
||||||
|
|
||||||
|
- after each change, run "make fmt".
|
||||||
|
|
||||||
|
- after each change, run "make test" and ensure all tests pass.
|
||||||
|
|
||||||
|
- after each change, run "make lint" and ensure no linting errors. fix any
|
||||||
|
you find, one by one.
|
||||||
|
|
||||||
|
- after each change, commit the files you've changed. push after
|
||||||
|
committing.
|
||||||
|
|
||||||
|
- NEVER use `git add -A`. always add only individual files that you've changed.
|
||||||
69
Dockerfile
69
Dockerfile
@@ -1,38 +1,37 @@
|
|||||||
# Lint stage — fast feedback on formatting and lint issues
|
################################################################################
|
||||||
# golangci/golangci-lint:v2.0.2 (2026-03-14)
|
#2345678911234567892123456789312345678941234567895123456789612345678971234567898
|
||||||
FROM golangci/golangci-lint@sha256:d55581f7797e7a0877a7c3aaa399b01bdc57d2874d6412601a046cc4062cb62e AS lint
|
################################################################################
|
||||||
|
FROM sneak/builder:2022-12-08 AS builder
|
||||||
WORKDIR /src
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
COPY go.mod go.sum ./
|
WORKDIR /build
|
||||||
RUN go mod download
|
COPY ./Makefile ./.golangci.yml ./go.mod ./go.sum /build/
|
||||||
|
COPY ./vendor.tzst /build/vendor.tzst
|
||||||
COPY . .
|
COPY ./modcache.tzst /build/modcache.tzst
|
||||||
|
COPY ./internal ./internal
|
||||||
# Touch .pb.go so make does not try to regenerate via protoc (file is committed)
|
COPY ./bin/gitrev.sh ./bin/gitrev.sh
|
||||||
RUN touch mfer/mf.pb.go
|
COPY ./mfer ./mfer
|
||||||
|
COPY ./cmd ./cmd
|
||||||
RUN make fmt-check
|
ARG GITREV unknown
|
||||||
RUN make lint
|
ARG DRONE_COMMIT_SHA unknown
|
||||||
|
|
||||||
# Build stage — tests and compilation
|
|
||||||
# golang:1.23 (2026-03-14)
|
|
||||||
FROM golang@sha256:60deed95d3888cc5e4d9ff8a10c54e5edc008c6ae3fba6187be6fb592e19e8c0 AS builder
|
|
||||||
|
|
||||||
# Force BuildKit to run the lint stage by creating a stage dependency
|
|
||||||
COPY --from=lint /src/go.sum /dev/null
|
|
||||||
|
|
||||||
WORKDIR /src
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
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 cd cmd/mfer && go build -tags urfave_cli_no_docs -o /mfer .
|
|
||||||
|
|
||||||
|
RUN mkdir -p "$(go env GOMODCACHE)" && cd "$(go env GOMODCACHE)" && \
|
||||||
|
zstdmt -d --stdout /build/modcache.tzst | tar xf - && \
|
||||||
|
rm /build/modcache.tzst && cd /build
|
||||||
|
RUN \
|
||||||
|
cd mfer && go generate . && cd .. && \
|
||||||
|
GOPACKAGESDEBUG=true golangci-lint run ./... && \
|
||||||
|
mkdir vendor && cd vendor && \
|
||||||
|
zstdmt -d --stdout /build/vendor.tzst | tar xf - && rm /build/vendor.tzst && \
|
||||||
|
cd .. && \
|
||||||
|
make mfer.cmd
|
||||||
|
RUN rm -rf /build/vendor && go mod vendor && tar -c . | zstdmt -19 > /src.tzst
|
||||||
|
################################################################################
|
||||||
|
#2345678911234567892123456789312345678941234567895123456789612345678971234567898
|
||||||
|
################################################################################
|
||||||
|
## final image
|
||||||
|
################################################################################
|
||||||
FROM scratch
|
FROM scratch
|
||||||
COPY --from=builder /mfer /mfer
|
# we put all the source into the final image for posterity, it's small
|
||||||
|
COPY --from=builder /src.tzst /src.tzst
|
||||||
|
COPY --from=builder /build/mfer.cmd /mfer
|
||||||
ENTRYPOINT ["/mfer"]
|
ENTRYPOINT ["/mfer"]
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ See [`mfer/mf.proto`](mfer/mf.proto) for exact field numbers and types.
|
|||||||
The outer message contains:
|
The outer message contains:
|
||||||
|
|
||||||
| Field | Number | Type | Description |
|
| Field | Number | Type | Description |
|
||||||
| ----------------- | ------ | ---------------- | ------------------------------------------------------------------------ |
|
|--------------------|--------|-------------------|--------------------------------------------------|
|
||||||
| `version` | 101 | enum | Must be `VERSION_ONE` (1) |
|
| `version` | 101 | enum | Must be `VERSION_ONE` (1) |
|
||||||
| `compressionType` | 102 | enum | Compression of `innerMessage`; must be `COMPRESSION_ZSTD` (1) |
|
| `compressionType` | 102 | enum | Compression of `innerMessage`; must be `COMPRESSION_ZSTD` (1) |
|
||||||
| `size` | 103 | int64 | Uncompressed size of `innerMessage` (corruption detection) |
|
| `size` | 103 | int64 | Uncompressed size of `innerMessage` (corruption detection) |
|
||||||
@@ -55,7 +55,7 @@ After decompressing `innerMessage`, the result is a serialized `MFFile`
|
|||||||
(referred to as the manifest):
|
(referred to as the manifest):
|
||||||
|
|
||||||
| Field | Number | Type | Description |
|
| Field | Number | Type | Description |
|
||||||
| ----------- | ------ | --------------------- | ------------------------------------- |
|
|-------------|--------|-----------------------|--------------------------------------------|
|
||||||
| `version` | 100 | enum | Must be `VERSION_ONE` (1) |
|
| `version` | 100 | enum | Must be `VERSION_ONE` (1) |
|
||||||
| `files` | 101 | repeated `MFFilePath` | List of files in the manifest |
|
| `files` | 101 | repeated `MFFilePath` | List of files in the manifest |
|
||||||
| `uuid` | 102 | bytes | Random v4 UUID; must match outer UUID |
|
| `uuid` | 102 | bytes | Random v4 UUID; must match outer UUID |
|
||||||
@@ -66,7 +66,7 @@ After decompressing `innerMessage`, the result is a serialized `MFFile`
|
|||||||
Each file entry contains:
|
Each file entry contains:
|
||||||
|
|
||||||
| Field | Number | Type | Description |
|
| Field | Number | Type | Description |
|
||||||
| ---------- | ------ | ------------------------- | ----------------------------------- |
|
|------------|--------|---------------------------|--------------------------------------|
|
||||||
| `path` | 1 | string | Relative file path (see Path Rules) |
|
| `path` | 1 | string | Relative file path (see Path Rules) |
|
||||||
| `size` | 2 | int64 | File size in bytes |
|
| `size` | 2 | int64 | File size in bytes |
|
||||||
| `hashes` | 3 | repeated `MFFileChecksum` | At least one hash required |
|
| `hashes` | 3 | repeated `MFFileChecksum` | At least one hash required |
|
||||||
@@ -111,7 +111,6 @@ ZNAVSRFG-<UUID>-<SHA256>
|
|||||||
```
|
```
|
||||||
|
|
||||||
Where:
|
Where:
|
||||||
|
|
||||||
- `ZNAVSRFG` is the magic bytes string (literal ASCII)
|
- `ZNAVSRFG` is the magic bytes string (literal ASCII)
|
||||||
- `<UUID>` is the hex-encoded UUID from the outer message
|
- `<UUID>` is the hex-encoded UUID from the outer message
|
||||||
- `<SHA256>` is the hex-encoded SHA-256 hash from the outer message (covering compressed data)
|
- `<SHA256>` is the hex-encoded SHA-256 hash from the outer message (covering compressed data)
|
||||||
|
|||||||
15
Makefile
15
Makefile
@@ -5,7 +5,7 @@ export PATH := $(PATH):$(GOPATH)/bin
|
|||||||
PROTOC_GEN_GO := $(GOPATH)/bin/protoc-gen-go
|
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)
|
||||||
APPNAME := mfer
|
APPNAME := mfer
|
||||||
VERSION := 0.1.0
|
VERSION := 0.1.0
|
||||||
export DOCKER_IMAGE_CACHE_DIR := $(HOME)/Library/Caches/Docker/$(APPNAME)-$(ARCH)
|
export DOCKER_IMAGE_CACHE_DIR := $(HOME)/Library/Caches/Docker/$(APPNAME)-$(ARCH)
|
||||||
@@ -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: docker default run ci test check lint fmt fmt-check hooks fixme
|
.PHONY: docker default run ci test fixme
|
||||||
|
|
||||||
default: fmt test
|
default: fmt test
|
||||||
|
|
||||||
@@ -32,17 +32,8 @@ $(PROTOC_GEN_GO):
|
|||||||
fixme:
|
fixme:
|
||||||
@grep -nir fixme . | grep -v Makefile
|
@grep -nir fixme . | grep -v Makefile
|
||||||
|
|
||||||
check: test lint fmt-check
|
|
||||||
|
|
||||||
fmt-check: mfer/mf.pb.go
|
|
||||||
sh -c 'test -z "$$(gofmt -l .)"'
|
|
||||||
|
|
||||||
hooks:
|
|
||||||
echo '#!/bin/sh\nmake check' > .git/hooks/pre-commit
|
|
||||||
chmod +x .git/hooks/pre-commit
|
|
||||||
|
|
||||||
devprereqs:
|
devprereqs:
|
||||||
which golangci-lint || go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@v2.0.2
|
which golangci-lint || go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@latest
|
||||||
|
|
||||||
mfer/mf.pb.go: mfer/mf.proto
|
mfer/mf.pb.go: mfer/mf.proto
|
||||||
cd mfer && go generate .
|
cd mfer && go generate .
|
||||||
|
|||||||
82
README.md
82
README.md
@@ -9,9 +9,8 @@ cryptographic checksums or signatures over same) to aid in archiving,
|
|||||||
downloading, and streaming, or mirroring. The manifest files' data is
|
downloading, and streaming, or mirroring. The manifest files' data is
|
||||||
serialized with Google's [protobuf serialization
|
serialized with Google's [protobuf serialization
|
||||||
format](https://developers.google.com/protocol-buffers). The structure of
|
format](https://developers.google.com/protocol-buffers). The structure of
|
||||||
these files can be found [in the format
|
these files can be found in the [format specification](FORMAT.md) and the
|
||||||
specification](https://git.eeqj.de/sneak/mfer/src/branch/main/mfer/mf.proto)
|
[protobuf schema](mfer/mf.proto), both included in the [project
|
||||||
which is included in the [project
|
|
||||||
repository](https://git.eeqj.de/sneak/mfer).
|
repository](https://git.eeqj.de/sneak/mfer).
|
||||||
|
|
||||||
The current version is pre-1.0 and while the repo was published in 2022,
|
The current version is pre-1.0 and while the repo was published in 2022,
|
||||||
@@ -23,6 +22,66 @@ itch in 2022 and is currently a one-person effort, though the goal is for
|
|||||||
this to emerge as a de-facto standard and be incorporated into other
|
this to emerge as a de-facto standard and be incorporated into other
|
||||||
software. A compatible javascript library is planned.
|
software. A compatible javascript library is planned.
|
||||||
|
|
||||||
|
# Phases
|
||||||
|
|
||||||
|
Manifest generation happens in two distinct phases:
|
||||||
|
|
||||||
|
## Phase 1: Enumeration
|
||||||
|
|
||||||
|
Walking directories and calling `stat()` on files to collect metadata (path, size, mtime, ctime). This builds the list of files to be scanned. Relatively fast as it only reads filesystem metadata, not file contents.
|
||||||
|
|
||||||
|
**Progress:** `EnumerateStatus` with `FilesFound` and `BytesFound`
|
||||||
|
|
||||||
|
## Phase 2: Scan (ToManifest)
|
||||||
|
|
||||||
|
Reading file contents and computing cryptographic hashes for manifest generation. This is the expensive phase that reads all file data from disk.
|
||||||
|
|
||||||
|
**Progress:** `ScanStatus` with `TotalFiles`, `ScannedFiles`, `TotalBytes`, `ScannedBytes`, `BytesPerSec`
|
||||||
|
|
||||||
|
# Code Conventions
|
||||||
|
|
||||||
|
- **Logging:** Never use `fmt.Printf` or write to stdout/stderr directly in normal code. Use the `internal/log` package for all output (`log.Info`, `log.Infof`, `log.Debug`, `log.Debugf`, `log.Progressf`, `log.ProgressDone`).
|
||||||
|
- **Filesystem abstraction:** Use `github.com/spf13/afero` for filesystem operations to enable testing and flexibility.
|
||||||
|
- **CLI framework:** Use `github.com/urfave/cli/v2` for command-line interface.
|
||||||
|
- **Serialization:** Use Protocol Buffers for manifest file format.
|
||||||
|
- **Internal packages:** Non-exported implementation details go in `internal/` subdirectories.
|
||||||
|
- **Concurrency:** Use `sync.RWMutex` for protecting shared state; prefer channels for progress reporting.
|
||||||
|
- **Progress channels:** Use buffered channels (size 1) with non-blocking sends to avoid blocking the main operation if the consumer is slow.
|
||||||
|
- **Context support:** Long-running operations should accept `context.Context` for cancellation.
|
||||||
|
- **NO_COLOR:** Respect the `NO_COLOR` environment variable for disabling colored output.
|
||||||
|
- **Options pattern:** Use `NewWithOptions(opts *Options)` constructor pattern for configurable types.
|
||||||
|
|
||||||
|
# Building
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Go 1.21 or later
|
||||||
|
- `protoc` (Protocol Buffers compiler) — only needed if modifying `.proto` files
|
||||||
|
- `golangci-lint` — for linting (`go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest`)
|
||||||
|
- `gofumpt` — for formatting (`go install mvdan.cc/gofumpt@latest`)
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# Build the binary
|
||||||
|
make bin/mfer
|
||||||
|
|
||||||
|
# Run tests
|
||||||
|
make test
|
||||||
|
|
||||||
|
# Format code
|
||||||
|
make fmt
|
||||||
|
|
||||||
|
# Lint
|
||||||
|
make lint
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install from source
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go install sneak.berlin/go/mfer/cmd/mfer@latest
|
||||||
|
```
|
||||||
|
|
||||||
# Build Status
|
# Build Status
|
||||||
|
|
||||||
[](https://drone.datavi.be/sneak/mfer)
|
[](https://drone.datavi.be/sneak/mfer)
|
||||||
@@ -42,9 +101,6 @@ requests](https://git.eeqj.de/sneak/mfer/pulls) and pass CI to be merged.
|
|||||||
Any changes submitted to this project must also be
|
Any changes submitted to this project must also be
|
||||||
[WTFPL-licensed](https://wtfpl.net) to be considered.
|
[WTFPL-licensed](https://wtfpl.net) to be considered.
|
||||||
|
|
||||||
See [`REPO_POLICIES.md`](REPO_POLICIES.md) for detailed coding standards,
|
|
||||||
tooling requirements, and workflow conventions.
|
|
||||||
|
|
||||||
# Problem Statement
|
# Problem Statement
|
||||||
|
|
||||||
Given a plain URL, there is no standard way to safely and programmatically
|
Given a plain URL, there is no standard way to safely and programmatically
|
||||||
@@ -122,9 +178,14 @@ The manifest file would do several important things:
|
|||||||
- metadata size should not be used as an excuse to sacrifice utility (such
|
- metadata size should not be used as an excuse to sacrifice utility (such
|
||||||
as providing checksums over each chunk of a large file)
|
as providing checksums over each chunk of a large file)
|
||||||
|
|
||||||
|
# Limitations
|
||||||
|
|
||||||
|
- **Manifest size:** Manifests must fit entirely in system memory during reading and writing.
|
||||||
|
|
||||||
# Open Questions
|
# Open Questions
|
||||||
|
|
||||||
- Should the manifest file include checksums of individual file chunks, or just for the whole assembled file?
|
- Should the manifest file include checksums of individual file chunks, or just for the whole assembled file?
|
||||||
|
|
||||||
- If so, should the chunksize be fixed or dynamic?
|
- If so, should the chunksize be fixed or dynamic?
|
||||||
|
|
||||||
- Should the manifest signature format be GnuPG signatures, or those from
|
- Should the manifest signature format be GnuPG signatures, or those from
|
||||||
@@ -208,15 +269,6 @@ regardless of filesystem format.
|
|||||||
Please email [`sneak@sneak.berlin`](mailto:sneak@sneak.berlin) with your
|
Please email [`sneak@sneak.berlin`](mailto:sneak@sneak.berlin) with your
|
||||||
desired username for an account on this Gitea instance.
|
desired username for an account on this Gitea instance.
|
||||||
|
|
||||||
# See Also
|
|
||||||
|
|
||||||
## Prior Art: Metalink
|
|
||||||
|
|
||||||
- [Metalink - Mozilla Wiki](https://wiki.mozilla.org/Metalink)
|
|
||||||
- [Metalink - Wikipedia](https://en.wikipedia.org/wiki/Metalink)
|
|
||||||
- [RFC 5854 - The Metalink Download Description Format](https://datatracker.ietf.org/doc/html/rfc5854)
|
|
||||||
- [RFC 6249 - Metalink/HTTP: Mirrors and Hashes](https://www.rfc-editor.org/rfc/rfc6249.html)
|
|
||||||
|
|
||||||
## Links
|
## Links
|
||||||
|
|
||||||
- Repo: [https://git.eeqj.de/sneak/mfer](https://git.eeqj.de/sneak/mfer)
|
- Repo: [https://git.eeqj.de/sneak/mfer](https://git.eeqj.de/sneak/mfer)
|
||||||
|
|||||||
255
REPO_POLICIES.md
255
REPO_POLICIES.md
@@ -1,255 +0,0 @@
|
|||||||
---
|
|
||||||
title: Repository Policies
|
|
||||||
last_modified: 2026-03-10
|
|
||||||
---
|
|
||||||
|
|
||||||
This document covers repository structure, tooling, and workflow standards. Code
|
|
||||||
style conventions are in separate documents:
|
|
||||||
|
|
||||||
- [Code Styleguide](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE.md)
|
|
||||||
(general, bash, Docker)
|
|
||||||
- [Go](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE_GO.md)
|
|
||||||
- [JavaScript](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE_JS.md)
|
|
||||||
- [Python](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/CODE_STYLEGUIDE_PYTHON.md)
|
|
||||||
- [Go HTTP Server Conventions](https://git.eeqj.de/sneak/prompts/raw/branch/main/prompts/GO_HTTP_SERVER_CONVENTIONS.md)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
- Cross-project documentation (such as this file) must include
|
|
||||||
`last_modified: YYYY-MM-DD` in the YAML front matter so it can be kept in sync
|
|
||||||
with the authoritative source as policies evolve.
|
|
||||||
|
|
||||||
- **ALL external references must be pinned by cryptographic hash.** This
|
|
||||||
includes Docker base images, Go modules, npm packages, GitHub Actions, and
|
|
||||||
anything else fetched from a remote source. Version tags (`@v4`, `@latest`,
|
|
||||||
`:3.21`, etc.) are server-mutable and therefore remote code execution
|
|
||||||
vulnerabilities. The ONLY acceptable way to reference an external dependency
|
|
||||||
is by its content hash (Docker `@sha256:...`, Go module hash in `go.sum`, npm
|
|
||||||
integrity hash in lockfile, GitHub Actions `@<commit-sha>`). No exceptions.
|
|
||||||
This also means never `curl | bash` to install tools like pyenv, nvm, rustup,
|
|
||||||
etc. Instead, download a specific release archive from GitHub, verify its hash
|
|
||||||
(hardcoded in the Dockerfile or script), and only then install. Unverified
|
|
||||||
install scripts are arbitrary remote code execution. This is the single most
|
|
||||||
important rule in this document. Double-check every external reference in
|
|
||||||
every file before committing. There are zero exceptions to this rule.
|
|
||||||
|
|
||||||
- Every repo with software must have a root `Makefile` with these targets:
|
|
||||||
`make test`, `make lint`, `make fmt` (writes), `make fmt-check` (read-only),
|
|
||||||
`make check` (prereqs: `test`, `lint`, `fmt-check`), `make docker`, and
|
|
||||||
`make hooks` (installs pre-commit hook). A model Makefile is at
|
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/Makefile`.
|
|
||||||
|
|
||||||
- Always use Makefile targets (`make fmt`, `make test`, `make lint`, etc.)
|
|
||||||
instead of invoking the underlying tools directly. The Makefile is the single
|
|
||||||
source of truth for how these operations are run.
|
|
||||||
|
|
||||||
- The Makefile is authoritative documentation for how the repo is used. Beyond
|
|
||||||
the required targets above, it should have targets for every common operation:
|
|
||||||
running a local development server (`make run`, `make dev`), re-initializing
|
|
||||||
or migrating the database (`make db-reset`, `make migrate`), building
|
|
||||||
artifacts (`make build`), generating code, seeding data, or anything else a
|
|
||||||
developer would do regularly. If someone checks out the repo and types
|
|
||||||
`make<tab>`, they should see every meaningful operation available. A new
|
|
||||||
contributor should be able to understand the entire development workflow by
|
|
||||||
reading the Makefile.
|
|
||||||
|
|
||||||
- Every repo should have a `Dockerfile`. All Dockerfiles must run `make check`
|
|
||||||
as a build step so the build fails if the branch is not green. For non-server
|
|
||||||
repos, the Dockerfile should bring up a development environment and run
|
|
||||||
`make check`. For server repos, `make check` should run as an early build
|
|
||||||
stage before the final image is assembled.
|
|
||||||
|
|
||||||
- Every repo should have a Gitea Actions workflow (`.gitea/workflows/`) that
|
|
||||||
runs `docker build .` on push. Since the Dockerfile already runs `make check`,
|
|
||||||
a successful build implies all checks pass.
|
|
||||||
|
|
||||||
- Use platform-standard formatters: `black` for Python, `prettier` for
|
|
||||||
JS/CSS/Markdown/HTML, `go fmt` for Go. Always use default configuration with
|
|
||||||
two exceptions: four-space indents (except Go), and `proseWrap: always` for
|
|
||||||
Markdown (hard-wrap at 80 columns). Documentation and writing repos (Markdown,
|
|
||||||
HTML, CSS) should also have `.prettierrc` and `.prettierignore`.
|
|
||||||
|
|
||||||
- Pre-commit hook: `make check` if local testing is possible, otherwise
|
|
||||||
`make lint && make fmt-check`. The Makefile should provide a `make hooks`
|
|
||||||
target to install the pre-commit hook.
|
|
||||||
|
|
||||||
- All repos with software must have tests that run via the platform-standard
|
|
||||||
test framework (`go test`, `pytest`, `jest`/`vitest`, etc.). If no meaningful
|
|
||||||
tests exist yet, add the most minimal test possible — e.g. importing the
|
|
||||||
module under test to verify it compiles/parses. There is no excuse for
|
|
||||||
`make test` to be a no-op.
|
|
||||||
|
|
||||||
- `make test` must complete in under 20 seconds. Add a 30-second timeout in the
|
|
||||||
Makefile.
|
|
||||||
|
|
||||||
- Docker builds must complete in under 5 minutes.
|
|
||||||
|
|
||||||
- `make check` must not modify any files in the repo. Tests may use temporary
|
|
||||||
directories.
|
|
||||||
|
|
||||||
- `main` must always pass `make check`, no exceptions.
|
|
||||||
|
|
||||||
- Never commit secrets. `.env` files, credentials, API keys, and private keys
|
|
||||||
must be in `.gitignore`. No exceptions.
|
|
||||||
|
|
||||||
- `.gitignore` should be comprehensive from the start: OS files (`.DS_Store`),
|
|
||||||
editor files (`.swp`, `*~`), language build artifacts, and `node_modules/`.
|
|
||||||
Fetch the standard `.gitignore` from
|
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.gitignore` when setting up
|
|
||||||
a new repo.
|
|
||||||
|
|
||||||
- **No build artifacts in version control.** Code-derived data (compiled
|
|
||||||
bundles, minified output, generated assets) must never be committed to the
|
|
||||||
repository if it can be avoided. The build process (e.g. Dockerfile, Makefile)
|
|
||||||
should generate these at build time. Notable exception: Go protobuf generated
|
|
||||||
files (`.pb.go`) ARE committed because repos need to work with `go get`, which
|
|
||||||
downloads code but does not execute code generation.
|
|
||||||
|
|
||||||
- Never use `git add -A` or `git add .`. Always stage files explicitly by name.
|
|
||||||
|
|
||||||
- Never force-push to `main`.
|
|
||||||
|
|
||||||
- Make all changes on a feature branch. You can do whatever you want on a
|
|
||||||
feature branch.
|
|
||||||
|
|
||||||
- `.golangci.yml` is standardized and must _NEVER_ be modified by an agent, only
|
|
||||||
manually by the user. Fetch from
|
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/.golangci.yml`.
|
|
||||||
|
|
||||||
- When pinning images or packages by hash, add a comment above the reference
|
|
||||||
with the version and date (YYYY-MM-DD).
|
|
||||||
|
|
||||||
- Use `yarn`, not `npm`.
|
|
||||||
|
|
||||||
- Write all dates as YYYY-MM-DD (ISO 8601).
|
|
||||||
|
|
||||||
- Simple projects should be configured with environment variables.
|
|
||||||
|
|
||||||
- Dockerized web services listen on port 8080 by default, overridable with
|
|
||||||
`PORT`.
|
|
||||||
|
|
||||||
- **HTTP/web services must be hardened for production internet exposure before
|
|
||||||
tagging 1.0.** This means full compliance with security best practices
|
|
||||||
including, without limitation, all of the following:
|
|
||||||
- **Security headers** on every response:
|
|
||||||
- `Strict-Transport-Security` (HSTS) with `max-age` of at least one year
|
|
||||||
and `includeSubDomains`.
|
|
||||||
- `Content-Security-Policy` (CSP) with a restrictive default policy
|
|
||||||
(`default-src 'self'` as a baseline, tightened per-resource as
|
|
||||||
needed). Never use `unsafe-inline` or `unsafe-eval` unless
|
|
||||||
unavoidable, and document the reason.
|
|
||||||
- `X-Frame-Options: DENY` (or `SAMEORIGIN` if framing is required).
|
|
||||||
Prefer the `frame-ancestors` CSP directive as the primary control.
|
|
||||||
- `X-Content-Type-Options: nosniff`.
|
|
||||||
- `Referrer-Policy: strict-origin-when-cross-origin` (or stricter).
|
|
||||||
- `Permissions-Policy` restricting access to browser features the
|
|
||||||
application does not use (camera, microphone, geolocation, etc.).
|
|
||||||
- **Request and response limits:**
|
|
||||||
- Maximum request body size enforced on all endpoints (e.g. Go
|
|
||||||
`http.MaxBytesReader`). Choose a sane default per-route; never accept
|
|
||||||
unbounded input.
|
|
||||||
- Maximum response body size where applicable (e.g. paginated APIs).
|
|
||||||
- `ReadTimeout` and `ReadHeaderTimeout` on the `http.Server` to defend
|
|
||||||
against slowloris attacks.
|
|
||||||
- `WriteTimeout` on the `http.Server`.
|
|
||||||
- `IdleTimeout` on the `http.Server`.
|
|
||||||
- Per-handler execution time limits via `context.WithTimeout` or
|
|
||||||
chi/stdlib `middleware.Timeout`.
|
|
||||||
- **Authentication and session security:**
|
|
||||||
- Rate limiting on password-based authentication endpoints. API keys are
|
|
||||||
high-entropy and not susceptible to brute force, so they are exempt.
|
|
||||||
- CSRF tokens on all state-mutating HTML forms. API endpoints
|
|
||||||
authenticated via `Authorization` header (Bearer token, API key) are
|
|
||||||
exempt because the browser does not attach these automatically.
|
|
||||||
- Passwords stored using bcrypt, scrypt, or argon2 — never plain-text,
|
|
||||||
MD5, or SHA.
|
|
||||||
- Session cookies set with `HttpOnly`, `Secure`, and `SameSite=Lax` (or
|
|
||||||
`Strict`) attributes.
|
|
||||||
- **Reverse proxy awareness:**
|
|
||||||
- True client IP detection when behind a reverse proxy
|
|
||||||
(`X-Forwarded-For`, `X-Real-IP`). The application must accept
|
|
||||||
forwarded headers only from a configured set of trusted proxy
|
|
||||||
addresses — never trust `X-Forwarded-For` unconditionally.
|
|
||||||
- **CORS:**
|
|
||||||
- Authenticated endpoints must restrict `Access-Control-Allow-Origin` to
|
|
||||||
an explicit allowlist of known origins. Wildcard (`*`) is acceptable
|
|
||||||
only for public, unauthenticated read-only APIs.
|
|
||||||
- **Error handling:**
|
|
||||||
- Internal errors must never leak stack traces, SQL queries, file paths,
|
|
||||||
or other implementation details to the client. Return generic error
|
|
||||||
messages in production; detailed errors only when `DEBUG` is enabled.
|
|
||||||
- **TLS:**
|
|
||||||
- Services never terminate TLS directly. They are always deployed behind
|
|
||||||
a TLS-terminating reverse proxy. The service itself listens on plain
|
|
||||||
HTTP. However, HSTS headers and `Secure` cookie flags must still be
|
|
||||||
set by the application so that the browser enforces HTTPS end-to-end.
|
|
||||||
|
|
||||||
This list is non-exhaustive. Apply defense-in-depth: if a standard security
|
|
||||||
hardening measure exists for HTTP services and is not listed here, it is
|
|
||||||
still expected. When in doubt, harden.
|
|
||||||
|
|
||||||
- `README.md` is the primary documentation. Required sections:
|
|
||||||
- **Description**: First line must include the project name, purpose,
|
|
||||||
category (web server, SPA, CLI tool, etc.), license, and author. Example:
|
|
||||||
"µPaaS is an MIT-licensed Go web application by @sneak that receives
|
|
||||||
git-frontend webhooks and deploys applications via Docker in realtime."
|
|
||||||
- **Getting Started**: Copy-pasteable install/usage code block.
|
|
||||||
- **Rationale**: Why does this exist?
|
|
||||||
- **Design**: How is the program structured?
|
|
||||||
- **TODO**: Update meticulously, even between commits. When planning, put
|
|
||||||
the todo list in the README so a new agent can pick up where the last one
|
|
||||||
left off.
|
|
||||||
- **License**: MIT, GPL, or WTFPL. Ask the user for new projects. Include a
|
|
||||||
`LICENSE` file in the repo root and a License section in the README.
|
|
||||||
- **Author**: [@sneak](https://sneak.berlin).
|
|
||||||
|
|
||||||
- First commit of a new repo should contain only `README.md`.
|
|
||||||
|
|
||||||
- Go module root: `sneak.berlin/go/<name>`. Always run `go mod tidy` before
|
|
||||||
committing.
|
|
||||||
|
|
||||||
- Use SemVer.
|
|
||||||
|
|
||||||
- Database migrations live in `internal/db/migrations/` and must be embedded in
|
|
||||||
the binary.
|
|
||||||
- `000_migration.sql` — contains ONLY the creation of the migrations
|
|
||||||
tracking table itself. Nothing else.
|
|
||||||
- `001_schema.sql` — the full application schema.
|
|
||||||
- **Pre-1.0.0:** never add additional migration files (002, 003, etc.).
|
|
||||||
There is no installed base to migrate. Edit `001_schema.sql` directly.
|
|
||||||
- **Post-1.0.0:** add new numbered migration files for each schema change.
|
|
||||||
Never edit existing migrations after release.
|
|
||||||
|
|
||||||
- All repos should have an `.editorconfig` enforcing the project's indentation
|
|
||||||
settings.
|
|
||||||
|
|
||||||
- Avoid putting files in the repo root unless necessary. Root should contain
|
|
||||||
only project-level config files (`README.md`, `Makefile`, `Dockerfile`,
|
|
||||||
`LICENSE`, `.gitignore`, `.editorconfig`, `REPO_POLICIES.md`, and
|
|
||||||
language-specific config). Everything else goes in a subdirectory. Canonical
|
|
||||||
subdirectory names:
|
|
||||||
- `bin/` — executable scripts and tools
|
|
||||||
- `cmd/` — Go command entrypoints
|
|
||||||
- `configs/` — configuration templates and examples
|
|
||||||
- `deploy/` — deployment manifests (k8s, compose, terraform)
|
|
||||||
- `docs/` — documentation and markdown (README.md stays in root)
|
|
||||||
- `internal/` — Go internal packages
|
|
||||||
- `internal/db/migrations/` — database migrations
|
|
||||||
- `pkg/` — Go library packages
|
|
||||||
- `share/` — systemd units, data files
|
|
||||||
- `static/` — static assets (images, fonts, etc.)
|
|
||||||
- `web/` — web frontend source
|
|
||||||
|
|
||||||
- When setting up a new repo, files from the `prompts` repo may be used as
|
|
||||||
templates. Fetch them from
|
|
||||||
`https://git.eeqj.de/sneak/prompts/raw/branch/main/<path>`.
|
|
||||||
|
|
||||||
- New repos must contain at minimum:
|
|
||||||
- `README.md`, `.git`, `.gitignore`, `.editorconfig`
|
|
||||||
- `LICENSE`, `REPO_POLICIES.md` (copy from the `prompts` repo)
|
|
||||||
- `Makefile`
|
|
||||||
- `Dockerfile`, `.dockerignore`
|
|
||||||
- `.gitea/workflows/check.yml`
|
|
||||||
- Go: `go.mod`, `go.sum`, `.golangci.yml`
|
|
||||||
- JS: `package.json`, `yarn.lock`, `.prettierrc`, `.prettierignore`
|
|
||||||
- Python: `pyproject.toml`
|
|
||||||
44
TODO.md
44
TODO.md
@@ -2,83 +2,83 @@
|
|||||||
|
|
||||||
## Design Questions
|
## Design Questions
|
||||||
|
|
||||||
_sneak: please answer inline below each question. These are preserved for posterity._
|
*sneak: please answer inline below each question. These are preserved for posterity.*
|
||||||
|
|
||||||
### Format Design
|
### Format Design
|
||||||
|
|
||||||
**1. Should `MFFileChecksum` be simplified?**
|
**1. Should `MFFileChecksum` be simplified?**
|
||||||
Currently it's a separate message wrapping a single `bytes multiHash` field. Since multihash already self-describes the algorithm, `repeated bytes hashes` directly on `MFFilePath` would be simpler and reduce per-file protobuf overhead. Is the extra message layer intentional (e.g. planning to add per-hash metadata like `verified_at`)?
|
Currently it's a separate message wrapping a single `bytes multiHash` field. Since multihash already self-describes the algorithm, `repeated bytes hashes` directly on `MFFilePath` would be simpler and reduce per-file protobuf overhead. Is the extra message layer intentional (e.g. planning to add per-hash metadata like `verified_at`)?
|
||||||
|
|
||||||
> _answer:_
|
> *answer:* Leave as-is for now.
|
||||||
|
|
||||||
**2. Should file permissions/mode be stored?**
|
**2. Should file permissions/mode be stored?**
|
||||||
The format stores mtime/ctime but not Unix file permissions. For archival use (ExFAT, filesystem-independent checksums) this may not matter, but for software distribution or filesystem restoration it's a gap. Should we reserve a field now (e.g. `optional uint32 mode = 305`) even if we don't populate it yet?
|
The format stores mtime/ctime but not Unix file permissions. For archival use (ExFAT, filesystem-independent checksums) this may not matter, but for software distribution or filesystem restoration it's a gap. Should we reserve a field now (e.g. `optional uint32 mode = 305`) even if we don't populate it yet?
|
||||||
|
|
||||||
> _answer:_
|
> *answer:* No, not right now.
|
||||||
|
|
||||||
**3. Should `atime` be removed from the schema?**
|
**3. Should `atime` be removed from the schema?**
|
||||||
Access time is volatile, non-deterministic, and often disabled (`noatime`). Including it means two manifests of the same directory at different times will differ, which conflicts with the determinism goal. Remove it, or document it as "never set by default"?
|
Access time is volatile, non-deterministic, and often disabled (`noatime`). Including it means two manifests of the same directory at different times will differ, which conflicts with the determinism goal. Remove it, or document it as "never set by default"?
|
||||||
|
|
||||||
> _answer:_
|
> *answer:* REMOVED — done. Field 304 has been removed from the proto schema.
|
||||||
|
|
||||||
**4. What are the path normalization rules?**
|
**4. What are the path normalization rules?**
|
||||||
The proto has `string path` with no specification about: always forward-slash? Must be relative? No `..` components allowed? UTF-8 NFC vs NFD normalization (macOS vs Linux)? Max path length? This is a security issue (path traversal) and a cross-platform compatibility issue. What rules should the spec mandate?
|
The proto has `string path` with no specification about: always forward-slash? Must be relative? No `..` components allowed? UTF-8 NFC vs NFD normalization (macOS vs Linux)? Max path length? This is a security issue (path traversal) and a cross-platform compatibility issue. What rules should the spec mandate?
|
||||||
|
|
||||||
> _answer:_
|
> *answer:* Implemented — UTF-8, forward-slash only, relative paths only, no `..` segments. Documented in FORMAT.md.
|
||||||
|
|
||||||
**5. Should we add a version byte after the magic?**
|
**5. Should we add a version byte after the magic?**
|
||||||
Currently `ZNAVSRFG` is followed immediately by protobuf. Adding a version byte (`ZNAVSRFG\x01`) would allow future framing changes without requiring protobuf parsing to detect the version. `MFFileOuter.Version` serves this purpose but requires successful deserialization to read. Worth the extra byte?
|
Currently `ZNAVSRFG` is followed immediately by protobuf. Adding a version byte (`ZNAVSRFG\x01`) would allow future framing changes without requiring protobuf parsing to detect the version. `MFFileOuter.Version` serves this purpose but requires successful deserialization to read. Worth the extra byte?
|
||||||
|
|
||||||
> _answer:_
|
> *answer:* No — protobuf handles versioning via the `MFFileOuter.Version` field.
|
||||||
|
|
||||||
**6. Should we add a length-prefix after the magic?**
|
**6. Should we add a length-prefix after the magic?**
|
||||||
Protobuf is not self-delimiting. If we ever want to concatenate manifests or append data after the protobuf, the current framing is insufficient. Add a varint or fixed-width length-prefix?
|
Protobuf is not self-delimiting. If we ever want to concatenate manifests or append data after the protobuf, the current framing is insufficient. Add a varint or fixed-width length-prefix?
|
||||||
|
|
||||||
> _answer:_
|
> *answer:* Not needed now.
|
||||||
|
|
||||||
### Signature Design
|
### Signature Design
|
||||||
|
|
||||||
**7. What does the outer SHA-256 hash cover — compressed or uncompressed data?**
|
**7. What does the outer SHA-256 hash cover — compressed or uncompressed data?**
|
||||||
The review notes it currently hashes compressed data (good for verifying before decompression), but this should be explicitly documented. Which is the intended behavior?
|
The review notes it currently hashes compressed data (good for verifying before decompression), but this should be explicitly documented. Which is the intended behavior?
|
||||||
|
|
||||||
> _answer:_
|
> *answer:* Hash covers compressed data. Documented in FORMAT.md.
|
||||||
|
|
||||||
**8. Should `signatureString()` sign raw bytes instead of a hex-encoded string?**
|
**8. Should `signatureString()` sign raw bytes instead of a hex-encoded string?**
|
||||||
Currently the canonical string is `MAGIC-UUID-MULTIHASH` with hex encoding, which adds a transformation layer. Signing the raw `sha256` bytes (or compressed `innerMessage` directly) would be simpler. Keep the string format or switch to raw bytes?
|
Currently the canonical string is `MAGIC-UUID-MULTIHASH` with hex encoding, which adds a transformation layer. Signing the raw `sha256` bytes (or compressed `innerMessage` directly) would be simpler. Keep the string format or switch to raw bytes?
|
||||||
|
|
||||||
> _answer:_
|
> *answer:* Keep string format as-is (established).
|
||||||
|
|
||||||
**9. Should we support detached signature files (`.mf.sig`)?**
|
**9. Should we support detached signature files (`.mf.sig`)?**
|
||||||
Embedded signatures are better for single-file distribution. Detached `.mf.sig` files follow the familiar `SHASUMS`/`SHASUMS.asc` pattern and are simpler for HTTP serving. Support both modes?
|
Embedded signatures are better for single-file distribution. Detached `.mf.sig` files follow the familiar `SHASUMS`/`SHASUMS.asc` pattern and are simpler for HTTP serving. Support both modes?
|
||||||
|
|
||||||
> _answer:_
|
> *answer:* Not for 1.0.
|
||||||
|
|
||||||
**10. GPG vs pure-Go crypto for signatures?**
|
**10. GPG vs pure-Go crypto for signatures?**
|
||||||
Shelling out to `gpg` is fragile (may not be installed, version-dependent output). `github.com/ProtonMail/go-crypto` provides pure-Go OpenPGP, or we could go Ed25519/signify (simpler, no key management). Which direction?
|
Shelling out to `gpg` is fragile (may not be installed, version-dependent output). `github.com/ProtonMail/go-crypto` provides pure-Go OpenPGP, or we could go Ed25519/signify (simpler, no key management). Which direction?
|
||||||
|
|
||||||
> _answer:_
|
> *answer:* Keep GPG shelling for now (established).
|
||||||
|
|
||||||
### Implementation Design
|
### Implementation Design
|
||||||
|
|
||||||
**11. Should manifests be deterministic by default?**
|
**11. Should manifests be deterministic by default?**
|
||||||
This means: sort file entries by path, omit `createdAt` timestamp (or make it opt-in), no `atime`. Should determinism be the default, with a `--include-timestamps` flag to opt in?
|
This means: sort file entries by path, omit `createdAt` timestamp (or make it opt-in), no `atime`. Should determinism be the default, with a `--include-timestamps` flag to opt in?
|
||||||
|
|
||||||
> _answer:_
|
> *answer:* YES — implemented, default behavior.
|
||||||
|
|
||||||
**12. Should we consolidate or keep both scanner/checker implementations?**
|
**12. Should we consolidate or keep both scanner/checker implementations?**
|
||||||
There are two parallel implementations: `mfer/scanner.go` + `mfer/checker.go` (typed with `FileSize`, `RelFilePath`) and `internal/scanner/` + `internal/checker/` (raw `int64`, `string`). The `mfer/` versions are superior. Delete the `internal/` versions?
|
There are two parallel implementations: `mfer/scanner.go` + `mfer/checker.go` (typed with `FileSize`, `RelFilePath`) and `internal/scanner/` + `internal/checker/` (raw `int64`, `string`). The `mfer/` versions are superior. Delete the `internal/` versions?
|
||||||
|
|
||||||
> _answer:_
|
> *answer:* Consolidated — done (PR#27).
|
||||||
|
|
||||||
**13. Should the `manifest` type be exported?**
|
**13. Should the `manifest` type be exported?**
|
||||||
Currently unexported with exported constructors (`New`, `NewFromPaths`, etc.). Consumers can't declare `var m *mfer.manifest`. Export the type, or define an interface?
|
Currently unexported with exported constructors (`New`, `NewFromPaths`, etc.). Consumers can't declare `var m *mfer.manifest`. Export the type, or define an interface?
|
||||||
|
|
||||||
> _answer:_
|
> *answer:* Keep unexported.
|
||||||
|
|
||||||
**14. What should the Go module path be for 1.0?**
|
**14. What should the Go module path be for 1.0?**
|
||||||
Currently mixed between `sneak.berlin/go/mfer` and `git.eeqj.de/sneak/mfer`. Which is canonical?
|
Currently mixed between `sneak.berlin/go/mfer` and `git.eeqj.de/sneak/mfer`. Which is canonical?
|
||||||
|
|
||||||
> _answer:_
|
> *answer:* `sneak.berlin/go/mfer`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -86,19 +86,19 @@ Currently mixed between `sneak.berlin/go/mfer` and `git.eeqj.de/sneak/mfer`. Whi
|
|||||||
|
|
||||||
### Phase 1: Foundation (format correctness)
|
### Phase 1: Foundation (format correctness)
|
||||||
|
|
||||||
- [ ] Delete `internal/scanner/` and `internal/checker/` — consolidate on `mfer/` package versions; update CLI code
|
- [x] Delete `internal/scanner/` and `internal/checker/` — consolidate on `mfer/` package versions; update CLI code
|
||||||
- [ ] Add deterministic file ordering — sort entries by path (lexicographic, byte-order) in `Builder.Build()`; add test asserting byte-identical output from two runs
|
- [x] Add deterministic file ordering — sort entries by path (lexicographic, byte-order) in `Builder.Build()`; add test asserting byte-identical output from two runs
|
||||||
- [ ] Add decompression size limit — `io.LimitReader` in `deserializeInner()` with `m.pbOuter.Size` as bound
|
- [x] Add decompression size limit — `io.LimitReader` in `deserializeInner()` with `m.pbOuter.Size` as bound
|
||||||
- [ ] Fix `errors.Is` dead code in checker — replace with `os.IsNotExist(err)` or `errors.Is(err, fs.ErrNotExist)`
|
- [ ] Fix `errors.Is` dead code in checker — replace with `os.IsNotExist(err)` or `errors.Is(err, fs.ErrNotExist)`
|
||||||
- [ ] Fix `AddFile` to verify size — check `totalRead == size` after reading, return error on mismatch
|
- [ ] Fix `AddFile` to verify size — check `totalRead == size` after reading, return error on mismatch
|
||||||
- [ ] Specify path invariants — add proto comments (UTF-8, forward-slash, relative, no `..`, no leading `/`); validate in `Builder.AddFile` and `Builder.AddFileWithHash`
|
- [x] Specify path invariants — add proto comments (UTF-8, forward-slash, relative, no `..`, no leading `/`); validate in `Builder.AddFile` and `Builder.AddFileWithHash`
|
||||||
|
|
||||||
### Phase 2: CLI polish
|
### Phase 2: CLI polish
|
||||||
|
|
||||||
- [ ] Fix flag naming — all CLI flags use kebab-case as primary (`--include-dotfiles`, `--follow-symlinks`)
|
- [ ] Fix flag naming — all CLI flags use kebab-case as primary (`--include-dotfiles`, `--follow-symlinks`)
|
||||||
- [ ] Fix URL construction in fetch — use `BaseURL.JoinPath()` or `url.JoinPath()` instead of string concatenation
|
- [ ] Fix URL construction in fetch — use `BaseURL.JoinPath()` or `url.JoinPath()` instead of string concatenation
|
||||||
- [ ] Add progress rate-limiting to Checker — throttle to once per second, matching Scanner
|
- [ ] Add progress rate-limiting to Checker — throttle to once per second, matching Scanner
|
||||||
- [ ] Add `--deterministic` flag (or make it default) — omit `createdAt`, sort files
|
- [x] Add `--deterministic` flag (or make it default) — omit `createdAt`, sort files
|
||||||
|
|
||||||
### Phase 3: Robustness
|
### Phase 3: Robustness
|
||||||
|
|
||||||
@@ -109,10 +109,10 @@ Currently mixed between `sneak.berlin/go/mfer` and `git.eeqj.de/sneak/mfer`. Whi
|
|||||||
|
|
||||||
### Phase 4: Format finalization
|
### Phase 4: Format finalization
|
||||||
|
|
||||||
- [ ] Remove or deprecate `atime` from proto (pending design question answer)
|
- [x] Remove or deprecate `atime` from proto (pending design question answer)
|
||||||
- [ ] Reserve `optional uint32 mode = 305` in `MFFilePath` for future file permissions
|
- [ ] Reserve `optional uint32 mode = 305` in `MFFilePath` for future file permissions
|
||||||
- [ ] Add version byte after magic — `ZNAVSRFG\x01` for format version 1
|
- [ ] Add version byte after magic — `ZNAVSRFG\x01` for format version 1
|
||||||
- [ ] Write format specification document — separate from README: magic, outer structure, compression, inner structure, path invariants, signature scheme, canonical serialization
|
- [x] Write format specification document — separate from README: magic, outer structure, compression, inner structure, path invariants, signature scheme, canonical serialization
|
||||||
|
|
||||||
### Phase 5: Release prep
|
### Phase 5: Release prep
|
||||||
|
|
||||||
|
|||||||
@@ -26,12 +26,6 @@ func (mfa *CLIApp) generateManifestOperation(ctx *cli.Context) error {
|
|||||||
Fs: mfa.Fs,
|
Fs: mfa.Fs,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set seed for deterministic UUID if provided
|
|
||||||
if seed := ctx.String("seed"); seed != "" {
|
|
||||||
opts.Seed = seed
|
|
||||||
log.Infof("using deterministic seed for manifest UUID")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up signing options if sign-key is provided
|
// Set up signing options if sign-key is provided
|
||||||
if signKey := ctx.String("sign-key"); signKey != "" {
|
if signKey := ctx.String("sign-key"); signKey != "" {
|
||||||
opts.SigningOptions = &mfer.SigningOptions{
|
opts.SigningOptions = &mfer.SigningOptions{
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/urfave/cli/v2"
|
"github.com/urfave/cli/v2"
|
||||||
)
|
)
|
||||||
@@ -19,8 +18,7 @@ func isHTTPURL(s string) bool {
|
|||||||
// The caller must close the returned reader.
|
// The caller must close the returned reader.
|
||||||
func (mfa *CLIApp) openManifestReader(pathOrURL string) (io.ReadCloser, error) {
|
func (mfa *CLIApp) openManifestReader(pathOrURL string) (io.ReadCloser, error) {
|
||||||
if isHTTPURL(pathOrURL) {
|
if isHTTPURL(pathOrURL) {
|
||||||
client := &http.Client{Timeout: 30 * time.Second}
|
resp, err := http.Get(pathOrURL) //nolint:gosec // user-provided URL is intentional
|
||||||
resp, err := client.Get(pathOrURL) //nolint:gosec // user-provided URL is intentional
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to fetch %s: %w", pathOrURL, err)
|
return nil, fmt.Errorf("failed to fetch %s: %w", pathOrURL, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,6 @@ func (mfa *CLIApp) run(args []string) {
|
|||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "include-dotfiles",
|
Name: "include-dotfiles",
|
||||||
Aliases: []string{"IncludeDotfiles"},
|
|
||||||
|
|
||||||
Usage: "Include dot (hidden) files (excluded by default)",
|
Usage: "Include dot (hidden) files (excluded by default)",
|
||||||
},
|
},
|
||||||
@@ -155,11 +154,6 @@ func (mfa *CLIApp) run(args []string) {
|
|||||||
Usage: "GPG key ID to sign the manifest with",
|
Usage: "GPG key ID to sign the manifest with",
|
||||||
EnvVars: []string{"MFER_SIGN_KEY"},
|
EnvVars: []string{"MFER_SIGN_KEY"},
|
||||||
},
|
},
|
||||||
&cli.StringFlag{
|
|
||||||
Name: "seed",
|
|
||||||
Usage: "Seed value for deterministic manifest UUID",
|
|
||||||
EnvVars: []string{"MFER_SEED"},
|
|
||||||
},
|
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "include-timestamps",
|
Name: "include-timestamps",
|
||||||
Usage: "Include createdAt timestamp in manifest (omitted by default for determinism)",
|
Usage: "Include createdAt timestamp in manifest (omitted by default for determinism)",
|
||||||
@@ -222,7 +216,6 @@ func (mfa *CLIApp) run(args []string) {
|
|||||||
},
|
},
|
||||||
&cli.BoolFlag{
|
&cli.BoolFlag{
|
||||||
Name: "include-dotfiles",
|
Name: "include-dotfiles",
|
||||||
Aliases: []string{"IncludeDotfiles"},
|
|
||||||
|
|
||||||
Usage: "Include dot (hidden) files (excluded by default)",
|
Usage: "Include dot (hidden) files (excluded by default)",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -90,15 +90,6 @@ type Builder struct {
|
|||||||
createdAt time.Time
|
createdAt time.Time
|
||||||
includeTimestamps bool
|
includeTimestamps bool
|
||||||
signingOptions *SigningOptions
|
signingOptions *SigningOptions
|
||||||
fixedUUID []byte // if set, use this UUID instead of generating one
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetSeed derives a deterministic UUID from the given seed string.
|
|
||||||
// The seed is hashed once with SHA-256 and the first 16 bytes are used
|
|
||||||
// as a fixed UUID for the manifest.
|
|
||||||
func (b *Builder) SetSeed(seed string) {
|
|
||||||
hash := sha256.Sum256([]byte(seed))
|
|
||||||
b.fixedUUID = hash[:16]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewBuilder creates a new Builder.
|
// NewBuilder creates a new Builder.
|
||||||
@@ -241,7 +232,7 @@ func (b *Builder) Build(w io.Writer) error {
|
|||||||
b.mu.Lock()
|
b.mu.Lock()
|
||||||
defer b.mu.Unlock()
|
defer b.mu.Unlock()
|
||||||
|
|
||||||
// Sort files by path for deterministic output
|
// Sort files by path for deterministic output (#23)
|
||||||
sort.Slice(b.files, func(i, j int) bool {
|
sort.Slice(b.files, func(i, j int) bool {
|
||||||
return b.files[i].Path < b.files[j].Path
|
return b.files[i].Path < b.files[j].Path
|
||||||
})
|
})
|
||||||
@@ -259,7 +250,6 @@ func (b *Builder) Build(w io.Writer) error {
|
|||||||
m := &manifest{
|
m := &manifest{
|
||||||
pbInner: inner,
|
pbInner: inner,
|
||||||
signingOptions: b.signingOptions,
|
signingOptions: b.signingOptions,
|
||||||
fixedUUID: b.fixedUUID,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate outer wrapper
|
// Generate outer wrapper
|
||||||
|
|||||||
@@ -115,52 +115,51 @@ func TestNewTimestampFromTimeExtremeDate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBuilderDeterministicOutput(t *testing.T) {
|
func TestBuilderBuildDeterministicOrder(t *testing.T) {
|
||||||
buildManifest := func() []byte {
|
// Regression test for #23: files should be sorted by path in the manifest
|
||||||
|
// to ensure deterministic output regardless of insertion order.
|
||||||
|
buildManifest := func(paths []string) []byte {
|
||||||
b := NewBuilder()
|
b := NewBuilder()
|
||||||
// Use a fixed createdAt and UUID so output is reproducible
|
for _, p := range paths {
|
||||||
b.createdAt = time.Date(2025, 1, 1, 0, 0, 0, 0, time.UTC)
|
content := []byte("content of " + p)
|
||||||
b.fixedUUID = make([]byte, 16) // all zeros
|
reader := bytes.NewReader(content)
|
||||||
|
_, err := b.AddFile(RelFilePath(p), FileSize(len(content)), ModTime(time.Now()), reader, nil)
|
||||||
mtime := ModTime(time.Date(2025, 6, 1, 0, 0, 0, 0, time.UTC))
|
|
||||||
|
|
||||||
// Add files in reverse order to test sorting
|
|
||||||
files := []struct {
|
|
||||||
path string
|
|
||||||
content string
|
|
||||||
}{
|
|
||||||
{"c/file.txt", "content c"},
|
|
||||||
{"a/file.txt", "content a"},
|
|
||||||
{"b/file.txt", "content b"},
|
|
||||||
}
|
|
||||||
for _, f := range files {
|
|
||||||
r := bytes.NewReader([]byte(f.content))
|
|
||||||
_, err := b.AddFile(RelFilePath(f.path), FileSize(len(f.content)), mtime, r, nil)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
err := b.Build(&buf)
|
require.NoError(t, b.Build(&buf))
|
||||||
require.NoError(t, err)
|
|
||||||
return buf.Bytes()
|
return buf.Bytes()
|
||||||
}
|
}
|
||||||
|
|
||||||
out1 := buildManifest()
|
// Build with files in two different orders
|
||||||
out2 := buildManifest()
|
order1 := []string{"z.txt", "a.txt", "m/b.txt", "m/a.txt", "b.txt"}
|
||||||
assert.Equal(t, out1, out2, "two builds with same input should produce byte-identical output")
|
order2 := []string{"b.txt", "m/a.txt", "a.txt", "z.txt", "m/b.txt"}
|
||||||
|
|
||||||
|
manifest1 := buildManifest(order1)
|
||||||
|
manifest2 := buildManifest(order2)
|
||||||
|
|
||||||
|
// Parse both and verify file order is sorted
|
||||||
|
m1, err := NewManifestFromReader(bytes.NewReader(manifest1))
|
||||||
|
require.NoError(t, err)
|
||||||
|
m2, err := NewManifestFromReader(bytes.NewReader(manifest2))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
files1 := m1.Files()
|
||||||
|
files2 := m2.Files()
|
||||||
|
require.Len(t, files1, 5)
|
||||||
|
require.Len(t, files2, 5)
|
||||||
|
|
||||||
|
// Both should have same order
|
||||||
|
for i := range files1 {
|
||||||
|
assert.Equal(t, files1[i].Path, files2[i].Path, "file %d path mismatch", i)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSetSeedDeterministic(t *testing.T) {
|
// Verify the order is lexicographic
|
||||||
b1 := NewBuilder()
|
assert.Equal(t, "a.txt", files1[0].Path)
|
||||||
b1.SetSeed("test-seed-value")
|
assert.Equal(t, "b.txt", files1[1].Path)
|
||||||
b2 := NewBuilder()
|
assert.Equal(t, "m/a.txt", files1[2].Path)
|
||||||
b2.SetSeed("test-seed-value")
|
assert.Equal(t, "m/b.txt", files1[3].Path)
|
||||||
assert.Equal(t, b1.fixedUUID, b2.fixedUUID, "same seed should produce same UUID")
|
assert.Equal(t, "z.txt", files1[4].Path)
|
||||||
assert.Len(t, b1.fixedUUID, 16, "UUID should be 16 bytes")
|
|
||||||
|
|
||||||
b3 := NewBuilder()
|
|
||||||
b3.SetSeed("different-seed")
|
|
||||||
assert.NotEqual(t, b1.fixedUUID, b3.fixedUUID, "different seeds should produce different UUIDs")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestValidatePath(t *testing.T) {
|
func TestValidatePath(t *testing.T) {
|
||||||
|
|||||||
14
mfer/gpg.go
14
mfer/gpg.go
@@ -20,7 +20,7 @@ type SigningOptions struct {
|
|||||||
// gpgSign creates a detached signature of the data using the specified key.
|
// gpgSign creates a detached signature of the data using the specified key.
|
||||||
// Returns the armored detached signature.
|
// Returns the armored detached signature.
|
||||||
func gpgSign(data []byte, keyID GPGKeyID) ([]byte, error) {
|
func gpgSign(data []byte, keyID GPGKeyID) ([]byte, error) {
|
||||||
cmd := exec.Command("gpg", "--batch", "--no-tty",
|
cmd := exec.Command("gpg",
|
||||||
"--detach-sign",
|
"--detach-sign",
|
||||||
"--armor",
|
"--armor",
|
||||||
"--local-user", string(keyID),
|
"--local-user", string(keyID),
|
||||||
@@ -42,7 +42,7 @@ func gpgSign(data []byte, keyID GPGKeyID) ([]byte, error) {
|
|||||||
// gpgExportPublicKey exports the public key for the specified key ID.
|
// gpgExportPublicKey exports the public key for the specified key ID.
|
||||||
// Returns the armored public key.
|
// Returns the armored public key.
|
||||||
func gpgExportPublicKey(keyID GPGKeyID) ([]byte, error) {
|
func gpgExportPublicKey(keyID GPGKeyID) ([]byte, error) {
|
||||||
cmd := exec.Command("gpg", "--batch", "--no-tty",
|
cmd := exec.Command("gpg",
|
||||||
"--export",
|
"--export",
|
||||||
"--armor",
|
"--armor",
|
||||||
string(keyID),
|
string(keyID),
|
||||||
@@ -65,7 +65,7 @@ func gpgExportPublicKey(keyID GPGKeyID) ([]byte, error) {
|
|||||||
|
|
||||||
// gpgGetKeyFingerprint gets the full fingerprint for a key ID.
|
// gpgGetKeyFingerprint gets the full fingerprint for a key ID.
|
||||||
func gpgGetKeyFingerprint(keyID GPGKeyID) ([]byte, error) {
|
func gpgGetKeyFingerprint(keyID GPGKeyID) ([]byte, error) {
|
||||||
cmd := exec.Command("gpg", "--batch", "--no-tty",
|
cmd := exec.Command("gpg",
|
||||||
"--with-colons",
|
"--with-colons",
|
||||||
"--fingerprint",
|
"--fingerprint",
|
||||||
string(keyID),
|
string(keyID),
|
||||||
@@ -114,7 +114,7 @@ func gpgExtractPubKeyFingerprint(pubKey []byte) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Import the public key into the temporary keyring
|
// Import the public key into the temporary keyring
|
||||||
importCmd := exec.Command("gpg", "--batch", "--no-tty",
|
importCmd := exec.Command("gpg",
|
||||||
"--homedir", tmpDir,
|
"--homedir", tmpDir,
|
||||||
"--import",
|
"--import",
|
||||||
pubKeyFile,
|
pubKeyFile,
|
||||||
@@ -126,7 +126,7 @@ func gpgExtractPubKeyFingerprint(pubKey []byte) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List keys to get fingerprint
|
// List keys to get fingerprint
|
||||||
listCmd := exec.Command("gpg", "--batch", "--no-tty",
|
listCmd := exec.Command("gpg",
|
||||||
"--homedir", tmpDir,
|
"--homedir", tmpDir,
|
||||||
"--with-colons",
|
"--with-colons",
|
||||||
"--fingerprint",
|
"--fingerprint",
|
||||||
@@ -184,7 +184,7 @@ func gpgVerify(data, signature, pubKey []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Import the public key into the temporary keyring
|
// Import the public key into the temporary keyring
|
||||||
importCmd := exec.Command("gpg", "--batch", "--no-tty",
|
importCmd := exec.Command("gpg",
|
||||||
"--homedir", tmpDir,
|
"--homedir", tmpDir,
|
||||||
"--import",
|
"--import",
|
||||||
pubKeyFile,
|
pubKeyFile,
|
||||||
@@ -196,7 +196,7 @@ func gpgVerify(data, signature, pubKey []byte) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify the signature
|
// Verify the signature
|
||||||
verifyCmd := exec.Command("gpg", "--batch", "--no-tty",
|
verifyCmd := exec.Command("gpg",
|
||||||
"--homedir", tmpDir,
|
"--homedir", tmpDir,
|
||||||
"--verify",
|
"--verify",
|
||||||
sigFile,
|
sigFile,
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ type manifest struct {
|
|||||||
pbOuter *MFFileOuter
|
pbOuter *MFFileOuter
|
||||||
output *bytes.Buffer
|
output *bytes.Buffer
|
||||||
signingOptions *SigningOptions
|
signingOptions *SigningOptions
|
||||||
fixedUUID []byte // if set, use this UUID instead of generating one
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *manifest) String() string {
|
func (m *manifest) String() string {
|
||||||
|
|||||||
@@ -339,7 +339,7 @@ type MFFilePath struct {
|
|||||||
// optional per-file metadata
|
// optional per-file metadata
|
||||||
MimeType *string `protobuf:"bytes,301,opt,name=mimeType,proto3,oneof" json:"mimeType,omitempty"`
|
MimeType *string `protobuf:"bytes,301,opt,name=mimeType,proto3,oneof" json:"mimeType,omitempty"`
|
||||||
Mtime *Timestamp `protobuf:"bytes,302,opt,name=mtime,proto3,oneof" json:"mtime,omitempty"`
|
Mtime *Timestamp `protobuf:"bytes,302,opt,name=mtime,proto3,oneof" json:"mtime,omitempty"`
|
||||||
Ctime *Timestamp `protobuf:"bytes,303,opt,name=ctime,proto3,oneof" json:"ctime,omitempty"`
|
Ctime *Timestamp `protobuf:"bytes,303,opt,name=ctime,proto3,oneof" json:"ctime,omitempty"` // Field 304 (atime) removed — not useful for integrity verification.
|
||||||
unknownFields protoimpl.UnknownFields
|
unknownFields protoimpl.UnknownFields
|
||||||
sizeCache protoimpl.SizeCache
|
sizeCache protoimpl.SizeCache
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ message MFFilePath {
|
|||||||
optional string mimeType = 301;
|
optional string mimeType = 301;
|
||||||
optional Timestamp mtime = 302;
|
optional Timestamp mtime = 302;
|
||||||
optional Timestamp ctime = 303;
|
optional Timestamp ctime = 303;
|
||||||
|
// Field 304 (atime) removed — not useful for integrity verification.
|
||||||
}
|
}
|
||||||
|
|
||||||
message MFFileChecksum {
|
message MFFileChecksum {
|
||||||
|
|||||||
@@ -48,7 +48,6 @@ type ScannerOptions struct {
|
|||||||
IncludeTimestamps bool // Include createdAt timestamp in manifest (default: omit for determinism)
|
IncludeTimestamps bool // Include createdAt timestamp in manifest (default: omit for determinism)
|
||||||
Fs afero.Fs // Filesystem to use, defaults to OsFs if nil
|
Fs afero.Fs // Filesystem to use, defaults to OsFs if nil
|
||||||
SigningOptions *SigningOptions // GPG signing options (nil = no signing)
|
SigningOptions *SigningOptions // GPG signing options (nil = no signing)
|
||||||
Seed string // If set, derive a deterministic UUID from this seed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileEntry represents a file that has been enumerated.
|
// FileEntry represents a file that has been enumerated.
|
||||||
@@ -281,9 +280,6 @@ func (s *Scanner) ToManifest(ctx context.Context, w io.Writer, progress chan<- S
|
|||||||
if s.options.SigningOptions != nil {
|
if s.options.SigningOptions != nil {
|
||||||
builder.SetSigningOptions(s.options.SigningOptions)
|
builder.SetSigningOptions(s.options.SigningOptions)
|
||||||
}
|
}
|
||||||
if s.options.Seed != "" {
|
|
||||||
builder.SetSeed(s.options.Seed)
|
|
||||||
}
|
|
||||||
|
|
||||||
var scannedFiles FileCount
|
var scannedFiles FileCount
|
||||||
var scannedBytes FileSize
|
var scannedBytes FileSize
|
||||||
|
|||||||
@@ -49,13 +49,8 @@ func (m *manifest) generateOuter() error {
|
|||||||
return errors.New("internal error")
|
return errors.New("internal error")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use fixed UUID if provided, otherwise generate a new one
|
// Generate UUID and set on inner message
|
||||||
var manifestUUID uuid.UUID
|
manifestUUID := uuid.New()
|
||||||
if len(m.fixedUUID) == 16 {
|
|
||||||
copy(manifestUUID[:], m.fixedUUID)
|
|
||||||
} else {
|
|
||||||
manifestUUID = uuid.New()
|
|
||||||
}
|
|
||||||
m.pbInner.Uuid = manifestUUID[:]
|
m.pbInner.Uuid = manifestUUID[:]
|
||||||
|
|
||||||
innerData, err := proto.MarshalOptions{Deterministic: true}.Marshal(m.pbInner)
|
innerData, err := proto.MarshalOptions{Deterministic: true}.Marshal(m.pbInner)
|
||||||
|
|||||||
BIN
modcache.tzst
Normal file
BIN
modcache.tzst
Normal file
Binary file not shown.
BIN
vendor.tzst
Normal file
BIN
vendor.tzst
Normal file
Binary file not shown.
Reference in New Issue
Block a user