1 Commits

Author SHA1 Message Date
803b1e69d4 Update golangci-lint to v2.12.2 with canonical config (closes #60)
All checks were successful
check / check (push) Successful in 53s
- Add canonical .golangci.yml (v2 schema, default: all, project
  thresholds for lll/funlen/cyclop/dupl)
- Bump golangci-lint pins from v2.0.2 to v2.12.2 in Makefile
  (go install, new /v2 module path) and Dockerfile (tagged+digest
  Debian image pin)
- Fix all lint findings surfaced by the new linter set across
  cmd/mfer, internal/bork, internal/cli, internal/log, and mfer:
  static sentinel errors (err113), context-aware HTTP and exec
  (noctx), guarded integer conversions and stricter permissions
  (gosec), named constants (mnd, goconst), function decomposition
  (funlen, cyclop, gocognit, nestif), declaration ordering
  (funcorder), t.Parallel/t.TempDir/t.Setenv adoption in tests
  (paralleltest, usetesting), protobuf getters (protogetter), plus
  formatting and style cleanups (wsl_v5, nlreturn, lll, revive,
  testifylint, and others)
- Serialize CLI runs in tests behind a mutex so parallel tests do
  not cross-wire the process-global logger's captured output

The decompositions are behavior-preserving. In particular:

- REPO_POLICIES.md is untouched and stays byte-identical to the
  authoritative copy in the prompts repo
- the mfer.manifest type stays unexported; whether to export it is an
  open owner design question (README question 13)
- directories created by fetch keep mode 0755, because fetched trees
  are content meant to be readable by other uids
- an absent MFFilePath.Mtime is handled explicitly and identically in
  freshen, list, and export rather than being read as the Unix epoch,
  which would classify every entry as changed and rewrite the manifest
  on every freshen
- every user-visible error message renders byte-identically to what it
  did before, with the err113 sentinels wrapped mid-sentence where
  needed; the rendered strings are now pinned by tests

Also fixes an argument-injection defect the lint pass surfaced: key IDs
reach gpg as bare positional arguments, so a key ID beginning with "-"
was parsed by gpg as an option. All positional arguments now follow an
explicit "--" end-of-options marker.

The symlink-escape gap in fetch's path handling, which sanitizePath
does not and cannot address, is filed separately as #86.
2026-08-10 13:56:38 +00:00
17 changed files with 355 additions and 501 deletions

View File

@@ -1,4 +1,3 @@
*.tmp *.tmp
*.dockerimage *.dockerimage
.git .git
node_modules

1
.gitignore vendored
View File

@@ -1,6 +1,5 @@
/bin/ /bin/
/tmp /tmp
/node_modules/
*.tmp *.tmp
*.dockerimage *.dockerimage
/vendor /vendor

View File

@@ -1,14 +0,0 @@
# REPO_POLICIES.md is a verbatim copy of an authoritative upstream
# document (sneak/prompts). Local tooling must never rewrite it: any
# reformatting is silent drift from the source of truth.
REPO_POLICIES.md
# User-owned configuration, copied verbatim from upstream. Not matched by
# the current prettier file set, but listed so widening that set can never
# start rewriting it.
.golangci.yml
# Dependencies and build output
node_modules/
vendor/
bin/

View File

@@ -1,4 +0,0 @@
{
"tabWidth": 4,
"proseWrap": "always"
}

View File

@@ -13,8 +13,7 @@ source for coding standards, formatting, linting, and workflow rules.
- After each change, run `make fmt`, then `make test`, then `make lint`. Fix any - After each change, run `make fmt`, then `make test`, then `make lint`. Fix any
failures before committing. failures before committing.
- After each change, commit only the files you've changed. Push after - After each change, commit only the files you've changed. Push after committing.
committing.
## Attribution ## Attribution
@@ -27,5 +26,5 @@ source for coding standards, formatting, linting, and workflow rules.
- The proto definition is in `mfer/mf.proto`; generated `.pb.go` files are - The proto definition is in `mfer/mf.proto`; generated `.pb.go` files are
committed (required for `go get` compatibility). committed (required for `go get` compatibility).
- The format specification is in `FORMAT.md`. - The format specification is in `FORMAT.md`.
- See the TODO section in `README.md` for the 1.0 implementation plan and open - See the TODO section in `README.md` for the 1.0 implementation plan
design questions. and open design questions.

View File

@@ -11,32 +11,15 @@ COPY . .
# Touch .pb.go so make does not try to regenerate via protoc (file is committed) # Touch .pb.go so make does not try to regenerate via protoc (file is committed)
RUN touch mfer/mf.pb.go RUN touch mfer/mf.pb.go
# Go half of fmt-check only: this image has no node, so no prettier. The RUN make fmt-check
# markdown half runs in the mdfmt stage below.
RUN make fmt-check-go
RUN make lint RUN make lint
# Markdown/JSON format stage — prettier needs node, which the Go images
# do not have. node:22.17.0-bookworm-slim (2026-08-09); ships node
# 22.17.0 and yarn 1.22.22, the versions script/bootstrap pins.
FROM node@sha256:b04ce4ae4e95b522112c2e5c52f781471a5cbc3b594527bcddedee9bc48c03a0 AS mdfmt
WORKDIR /src
COPY package.json yarn.lock ./
RUN yarn install --frozen-lockfile
COPY . .
# No make in this image; call the script entrypoint directly.
RUN script/prettier --check
# Build stage — tests and compilation # Build stage — tests and compilation
# golang:1.23 (2026-03-14) # golang:1.23 (2026-03-14)
FROM golang@sha256:60deed95d3888cc5e4d9ff8a10c54e5edc008c6ae3fba6187be6fb592e19e8c0 AS builder FROM golang@sha256:60deed95d3888cc5e4d9ff8a10c54e5edc008c6ae3fba6187be6fb592e19e8c0 AS builder
# Force BuildKit to run the lint and mdfmt stages by creating stage dependencies # Force BuildKit to run the lint stage by creating a stage dependency
COPY --from=lint /src/go.sum /dev/null COPY --from=lint /src/go.sum /dev/null
COPY --from=mdfmt /src/go.sum /dev/null
WORKDIR /src WORKDIR /src
COPY go.mod go.sum ./ COPY go.mod go.sum ./

View File

@@ -5,9 +5,9 @@ Version 1.0
## Overview ## Overview
An `.mf` file is a binary manifest that describes a directory tree of files, An `.mf` file is a binary manifest that describes a directory tree of files,
including their paths, sizes, and cryptographic checksums. It supports optional including their paths, sizes, and cryptographic checksums. It supports
GPG signatures for integrity verification and optional timestamps for metadata optional GPG signatures for integrity verification and optional timestamps
preservation. for metadata preservation.
## File Structure ## File Structure
@@ -39,15 +39,15 @@ The outer message contains:
### SHA-256 Hash ### SHA-256 Hash
The `sha256` field (104) covers the **compressed** `innerMessage` bytes. This The `sha256` field (104) covers the **compressed** `innerMessage` bytes.
allows verifying data integrity before decompression. This allows verifying data integrity before decompression.
## Compression ## Compression
The `innerMessage` field is compressed with The `innerMessage` field is compressed with [Zstandard (zstd)](https://facebook.github.io/zstd/).
[Zstandard (zstd)](https://facebook.github.io/zstd/). Implementations must Implementations must enforce a decompression size limit to prevent
enforce a decompression size limit to prevent decompression bombs. The reference decompression bombs. The reference implementation limits decompressed size to
implementation limits decompressed size to 256 MB. 256 MB.
## Inner Message (`MFFile`) ## Inner Message (`MFFile`)
@@ -114,11 +114,11 @@ 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 - `<SHA256>` is the hex-encoded SHA-256 hash from the outer message (covering compressed data)
compressed data)
Components are separated by hyphens. The signature is produced by GPG over this Components are separated by hyphens. The signature is produced by GPG over
canonical string and stored in the `signature` field of the outer message. this canonical string and stored in the `signature` field of the outer
message.
## Deterministic Serialization ## Deterministic Serialization
@@ -134,11 +134,10 @@ changed).
## MIME Type ## MIME Type
The recommended MIME type for `.mf` files is `application/octet-stream`. The The recommended MIME type for `.mf` files is `application/octet-stream`.
`.mf` file extension is the canonical identifier. The `.mf` file extension is the canonical identifier.
## Reference ## Reference
- Proto definition: [`mfer/mf.proto`](mfer/mf.proto) - Proto definition: [`mfer/mf.proto`](mfer/mf.proto)
- Reference implementation: - Reference implementation: [git.eeqj.de/sneak/mfer](https://git.eeqj.de/sneak/mfer)
[git.eeqj.de/sneak/mfer](https://git.eeqj.de/sneak/mfer)

View File

@@ -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 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 hooks fixme
default: fmt test default: fmt test
@@ -44,14 +44,6 @@ check:
fmt-check: fmt-check:
@script/fmt-check @script/fmt-check
# Halves of fmt-check, for environments that have only one toolchain:
# the Docker lint stage has Go but no node, the markdown stage the reverse.
fmt-check-go:
@script/fmt-check-go
fmt-check-md:
@script/prettier --check
hooks: hooks:
@script/install-precommit @script/install-precommit

459
README.md
View File

@@ -1,80 +1,76 @@
# mfer # mfer
[mfer](https://git.eeqj.de/sneak/mfer) is a reference implementation library and [mfer](https://git.eeqj.de/sneak/mfer) is a reference implementation library
thin wrapper command-line utility written in [Go](https://golang.org) and first and thin wrapper command-line utility written in [Go](https://golang.org)
published in 2022 under the [WTFPL](https://wtfpl.net) (public domain) license. and first published in 2022 under the [WTFPL](https://wtfpl.net) (public
It specifies and generates `.mf` manifest files over a directory tree of files domain) license. It specifies and generates `.mf` manifest files over a
to encapsulate metadata about them (such as cryptographic checksums or directory tree of files to encapsulate metadata about them (such as
signatures over same) to aid in archiving, downloading, and streaming, or cryptographic checksums or signatures over same) to aid in archiving,
mirroring. The manifest files' data is serialized with Google's downloading, and streaming, or mirroring. The manifest files' data is
[protobuf serialization format](https://developers.google.com/protocol-buffers). serialized with Google's [protobuf serialization
The structure of these files can be found format](https://developers.google.com/protocol-buffers). The structure of
[in the format specification](https://git.eeqj.de/sneak/mfer/src/branch/main/mfer/mf.proto) these files can be found [in the format
which is included in the [project repository](https://git.eeqj.de/sneak/mfer). specification](https://git.eeqj.de/sneak/mfer/src/branch/main/mfer/mf.proto)
which is included in the [project
repository](https://git.eeqj.de/sneak/mfer).
The current version is pre-1.0 and while the repo was published in 2022, there The current version is pre-1.0 and while the repo was published in 2022,
has not yet been any versioned release. [SemVer](https://semver.org) will be there has not yet been any versioned release. [SemVer](https://semver.org)
used for releases. will be used for releases.
This project was started by [@sneak](https://sneak.berlin) to scratch an itch in This project was started by [@sneak](https://sneak.berlin) to scratch an
2022 and is currently a one-person effort, though the goal is for this to emerge itch in 2022 and is currently a one-person effort, though the goal is for
as a de-facto standard and be incorporated into other software. A compatible this to emerge as a de-facto standard and be incorporated into other
javascript library is planned. software. A compatible javascript library is planned.
# Build Status # Build Status
CI runs via `script/cibuild` (`docker build .`), which executes `make check` CI runs via `script/cibuild` (`docker build .`), which executes `make
(formatting, linting, tests). The `main` branch must always be green. check` (formatting, linting, tests). The `main` branch must always be
green.
# Entrypoints # Entrypoints
This repository adheres to the This repository adheres to the
[Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all) [Scripts to Rule Them All](https://github.com/github/scripts-to-rule-them-all)
standard: normalized scripts in `script/` are the entrypoints for the standard: normalized scripts in `script/` are the entrypoints for the
development workflow, and the Makefile targets are thin shims that call them. We development workflow, and the Makefile targets are thin shims that call
provide: them. We provide:
- `script/bootstrap` — install all dependencies (Go, golangci-lint, Go module - `script/bootstrap` — install all dependencies (Go, golangci-lint, Go
download, and node/yarn plus the prettier version pinned in module download), idempotently
`package.json`/`yarn.lock`), idempotently
- `script/setup` — make a fresh clone ready for development: runs - `script/setup` — make a fresh clone ready for development: runs
`script/bootstrap`, then `script/install-precommit` `script/bootstrap`, then `script/install-precommit`
- `script/projectname` — output the project name (`mfer`); used by other scripts - `script/projectname` — output the project name (`mfer`); used by other
such as `script/docker` scripts 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
first if it is stale protobuf code first if it is stale
- `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 prettier for JSON/Markdown
- `script/prettier` — run prettier over the repository's canonical file set - `script/fmt-check` — check formatting without writing
(Markdown and JSON, minus `.prettierignore`) in the given mode, `--write` or - `script/check` — run `script/test`, `script/lint`, and
`--check`; the single definition of that file set, so `script/fmt` and `script/fmt-check`
`script/fmt-check` cannot disagree about it
- `script/fmt-check` — check formatting without writing: `script/fmt-check-go`
plus `script/prettier --check`
- `script/fmt-check-go` — the Go half of `script/fmt-check`, on its own, for the
Docker lint stage, whose image has no node
- `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: `docker build .` (the Dockerfile runs the - `script/cibuild` — CI entrypoint: `docker build .` (the Dockerfile
checks) runs the checks)
- `script/precommit` — pre-commit checks: `go mod tidy` verification, then - `script/precommit` — pre-commit checks: `go mod tidy` verification,
`script/check` then `script/check`
- `script/install-precommit` — install the git pre-commit hook that runs - `script/install-precommit` — install the git pre-commit hook that
`script/precommit` runs `script/precommit`
# Participation # Participation
The community is as yet nonexistent so there are no defined policies or norms The community is as yet nonexistent so there are no defined policies or
yet. Primary development happens on a privately-run Gitea instance at norms yet. Primary development happens on a privately-run Gitea instance at
[https://git.eeqj.de/sneak/mfer](https://git.eeqj.de/sneak/mfer) and issues are [https://git.eeqj.de/sneak/mfer](https://git.eeqj.de/sneak/mfer) and issues
[tracked there](https://git.eeqj.de/sneak/mfer/issues). are [tracked there](https://git.eeqj.de/sneak/mfer/issues).
Changes must always be formatted with a standard `go fmt`, syntactically valid, Changes must always be formatted with a standard `go fmt`, syntactically
and must pass the linting defined in the repository (presently only the valid, and must pass the linting defined in the repository (presently only
`golangci-lint` defaults), which can be run with a `make lint`. The `main` the `golangci-lint` defaults), which can be run with a `make lint`. The
branch is protected and all changes must be made via `main` branch is protected and all changes must be made via [pull
[pull requests](https://git.eeqj.de/sneak/mfer/pulls) and pass CI to be merged. 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.
@@ -85,57 +81,58 @@ tooling requirements, and workflow conventions.
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
download everything "under" that URL path. `wget -r` can traverse directory download everything "under" that URL path. `wget -r` can traverse directory
listings if they're enabled, but every server has a different format, and this listings if they're enabled, but every server has a different format, and
does not verify cryptographic integrity of the files, or enable them to be this does not verify cryptographic integrity of the files, or enable them to
fetched using a different protocol other than HTTP/s. be fetched using a different protocol other than HTTP/s.
Currently, the solution that people are using are sidecar files in the format of Currently, the solution that people are using are sidecar files in the
`SHASUMS` checksum files, as well as a `SHASUMS.asc` PGP detached signature. format of `SHASUMS` checksum files, as well as a `SHASUMS.asc` PGP detached
This is not checksum-algorithm-agnostic and the sidecar file is not always signature. This is not checksum-algorithm-agnostic and the sidecar file is
consistently named. not always consistently named.
Real issues I face: Real issues I face:
- when I plug in an ExFAT hard drive, I don't know if any files on the - when I plug in an ExFAT hard drive, I don't know if any files on the
filesystem are corrupted or missing filesystem are corrupted or missing
- current ad-hoc solution are `SHASUMS`/`SHASUMS.asc` files - current ad-hoc solution are `SHASUMS`/`SHASUMS.asc` files
- when I want to mirror an HTTP archive, I have to use special tools like - when I want to mirror an HTTP archive, I have to use special tools like
debmirror that understand the archive format debmirror that understand the archive format
- the debian repository metadata structure is hot garbage - the debian repository metadata structure is hot garbage
- when I download a large file via HTTP, I have no way of knowing if the file - when I download a large file via HTTP, I have no way of knowing if the
content is what it's supposed to be file content is what it's supposed to be
# Proposed Solution # Proposed Solution
A standard, a manifest file format, and a tool for generating same. A standard, a manifest file format, and a tool for generating same.
The manifest file would be called `index.mf`, and the tool for generating such The manifest file would be called `index.mf`, and the tool for generating such would be called `mfer`.
would be called `mfer`.
The manifest file would do several important things: The manifest file would do several important things:
- have a standard filename, so if given `https://example.com/downloadpackage/` - have a standard filename, so if given
one could fetch `https://example.com/downloadpackage/index.mf` to enumerate `https://example.com/downloadpackage/` one could fetch
the full directory listing. `https://example.com/downloadpackage/index.mf` to enumerate the full
directory listing.
- contain a version field for extensibility - contain a version field for extensibility
- contain structured data (protobuf, json, or cbor) - contain structured data (protobuf, json, or cbor)
- provide an inner signed container, so that the manifest file itself can embed - provide an inner signed container, so that the manifest file itself can
a signature and a public key alongside in a single file embed a signature and a public key alongside in a single file
- contain a list of files, each with a relative path to the manifest - contain a list of files, each with a relative path to the manifest
- contain manifest timestamp - contain manifest timestamp
- contain ctime/mtime information for files so that file metadata can be - contain ctime/mtime information for files so that file metadata can be
preserved preserved
- contain cryptographic checksums in several different algorithms for each file - contain cryptographic checksums in several different algorithms for each
- probably encoded with multihash to indicate algo + hash file
- sha256 at the minimum - probably encoded with multihash to indicate algo + hash
- would be nice to include an IPFS/IPLD CIDv1 root hash for each file, which - sha256 at the minimum
likely involves doing an ipfs file object chunking - would be nice to include an IPFS/IPLD CIDv1 root hash for each file,
- maybe even including the complete IPFS/IPLD directory tree objects and which likely involves doing an ipfs file object chunking
chunklists? - maybe even including the complete IPFS/IPLD directory tree objects and
- this is because generating an `index.mf` does not imply publishing on chunklists?
ipfs at that time - this is because generating an `index.mf` does not imply publishing on
- maybe a bittorrent chunklist for torrent client compatibility? perhaps a ipfs at that time
top-level infohash for the whole manifest? - maybe a bittorrent chunklist for torrent client compatibility? perhaps a
top-level infohash for the whole manifest?
# Design Goals # Design Goals
@@ -149,37 +146,37 @@ The manifest file would do several important things:
# Non-Goals # Non-Goals
- Manifest generation speed - Manifest generation speed
- likely involves IPFS chunking, bittorrent chunking, and several different - likely involves IPFS chunking, bittorrent chunking, and several
cryptographic hash functions over the entirety of each and every file different cryptographic hash functions over the entirety of each and
every file
- Small manifest file size (within reason) - Small manifest file size (within reason)
- 30MiB files are "small" these days, given modern storage/bandwidth - 30MiB files are "small" these days, given modern storage/bandwidth
- 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)
# Open Questions # Open Questions
- Should the manifest file include checksums of individual file chunks, or just - Should the manifest file include checksums of individual file chunks, or just for the whole assembled file?
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
OpenBSD's signify (of which there is a good OpenBSD's signify (of which there is a good [golang
[golang implementation](https://github.com/frankbraun/gosignify)? implementation](https://github.com/frankbraun/gosignify)?
- Should the on-disk serialization format be proto3 or json? - Should the on-disk serialization format be proto3 or json?
# Tool Examples # Tool Examples
- `mfer gen` / `mfer gen .` - `mfer gen` / `mfer gen .`
- recurses under current directory and writes out an `index.mf` - recurses under current directory and writes out an `index.mf`
- `mfer check` / `mfer check .` - `mfer check` / `mfer check .`
- verifies checksums of all files in manifest, displaying error and exiting - verifies checksums of all files in manifest, displaying error and
nonzero if any files are missing or corrupted exiting nonzero if any files are missing or corrupted
- `mfer fetch https://example.com/stuff/` - `mfer fetch https://example.com/stuff/`
- fetches `/stuff/index.mf` and downloads all files listed in manifest, - fetches `/stuff/index.mf` and downloads all files listed in manifest,
optionally resuming any that already exist locally, and assures optionally resuming any that already exist locally, and assures
cryptographic integrity of downloaded files. cryptographic integrity of downloaded files.
# Implementation Plan # Implementation Plan
@@ -196,31 +193,30 @@ The manifest file would do several important things:
# Hopes And Dreams # Hopes And Dreams
- `aria2c https://example.com/manifestdirectory/` - `aria2c https://example.com/manifestdirectory/`
- (fetches `https://example.com/manifestdirectory/index.mf`, downloads and - (fetches `https://example.com/manifestdirectory/index.mf`, downloads and
checksums all files, resumes any that exist locally already) checksums all files, resumes any that exist locally already)
- `mfer fetch https://example.com/manifestdirectory/` - `mfer fetch https://example.com/manifestdirectory/`
- a command line option to zero/omit mtime/ctime, as well as manifest timestamp, - a command line option to zero/omit mtime/ctime, as well as manifest
and sort all directory listings so that manifest file generation is timestamp, and sort all directory listings so that manifest file
deterministic/reproducible generation is deterministic/reproducible
- URL format - URL format `mfer fetch https://exmaple.com/manifestdirectory/?key=5539AD00DE4C42F3AFE11575052443F4DF2A55C2`
`mfer fetch https://exmaple.com/manifestdirectory/?key=5539AD00DE4C42F3AFE11575052443F4DF2A55C2` to assert in the URL which PGP signing key should be used in the manifest,
to assert in the URL which PGP signing key should be used in the manifest, so so that shared URLs have a cryptographic trust root
that shared URLs have a cryptographic trust root - a "well-known" key in the manifest that maps well known keys (could reuse
- a "well-known" key in the manifest that maps well known keys (could reuse the the http spec) to specific file paths in the manifest.
http spec) to specific file paths in the manifest. - example: a `berlin.sneak.app.slideshow` key that maps to a json
- example: a `berlin.sneak.app.slideshow` key that maps to a json slideshow slideshow config listing what image paths to show, and for how long, and
config listing what image paths to show, and for how long, and in what in what order
order
# Use Cases # Use Cases
## Web Images ## Web Images
I'd like to be able to put a bunch of images into a directory, generate a I'd like to be able to put a bunch of images into a directory, generate a
manifest, and then point a slideshow client (such as an ambient display, or a manifest, and then point a slideshow client (such as an ambient display, or
react app with the target directory in a query string arg) at that statically a react app with the target directory in a query string arg) at that
hosted directory, and have it discover the full list of images available at that statically hosted directory, and have it discover the full list of images
URL. available at that URL.
## Software Distribution ## Software Distribution
@@ -230,20 +226,20 @@ resumably by either HTTP or IPFS/BitTorrent without a .torrent file.
## Filesystem Archive Integrity ## Filesystem Archive Integrity
I use filesystems that don't include data checksums, and I would like a I use filesystems that don't include data checksums, and I would like a
cryptographically signed checksum file so that I can later verify that a set of cryptographically signed checksum file so that I can later verify that a set
archive files have not been modified, none are missing, and that the checksums of archive files have not been modified, none are missing, and that the
have not been altered in storage by a second party. checksums have not been altered in storage by a second party.
## Filesystem-Independent Checksums ## Filesystem-Independent Checksums
I would like to be able to plug in a hard drive or flash drive and, if there is I would like to be able to plug in a hard drive or flash drive and, if there
an `index.mf` in the root, automatically detect missing/corrupted files, is an `index.mf` in the root, automatically detect missing/corrupted files,
regardless of filesystem format. regardless of filesystem format.
# Collaboration # Collaboration
Please email [`sneak@sneak.berlin`](mailto:sneak@sneak.berlin) with your desired Please email [`sneak@sneak.berlin`](mailto:sneak@sneak.berlin) with your
username for an account on this Gitea instance. desired username for an account on this Gitea instance.
# TODO: Remaining Work for 1.0 # TODO: Remaining Work for 1.0
@@ -254,108 +250,114 @@ inline below each question.
### Format Design ### Format Design
**1. Should `MFFileChecksum` be simplified?** Currently it's a separate message **1. Should `MFFileChecksum` be simplified?** Currently it's a separate
wrapping a single `bytes multiHash` field. Since multihash already message wrapping a single `bytes multiHash` field. Since multihash
self-describes the algorithm, `repeated bytes hashes` directly on `MFFilePath` already self-describes the algorithm, `repeated bytes hashes` directly on
would be simpler and reduce per-file protobuf overhead. Is the extra message `MFFilePath` would be simpler and reduce per-file protobuf overhead. Is
layer intentional (e.g. planning to add per-hash metadata like `verified_at`)? the extra message layer intentional (e.g. planning to add per-hash
metadata like `verified_at`)?
> _answer:_ > _answer:_
**2. Should file permissions/mode be stored?** The format stores mtime/ctime but **2. Should file permissions/mode be stored?** The format stores
not Unix file permissions. For archival use this may not matter, but for mtime/ctime but not Unix file permissions. For archival use this may not
software distribution or filesystem restoration it's a gap. Should we reserve a matter, but for software distribution or filesystem restoration it's a
field now (e.g. `optional uint32 mode = 305`) even if we don't populate it yet? gap. Should we reserve a field now (e.g. `optional uint32 mode = 305`)
even if we don't populate it yet?
> _answer:_ > _answer:_
**3. Should `atime` be removed from the schema?** Access time is volatile, **3. Should `atime` be removed from the schema?** Access time is
non-deterministic, and often disabled (`noatime`). Including it means two volatile, non-deterministic, and often disabled (`noatime`). Including it
manifests of the same directory at different times will differ, which conflicts means two manifests of the same directory at different times will differ,
with the determinism goal. Remove it, or document it as "never set by default"? which conflicts with the determinism goal. Remove it, or document it as
"never set by default"?
> _answer:_ > _answer:_
**4. What are the path normalization rules?** The proto has `string path` with **4. What are the path normalization rules?** The proto has `string path`
no specification about: always forward-slash? Must be relative? No `..` with no specification about: always forward-slash? Must be relative? No
components allowed? UTF-8 NFC vs NFD normalization (macOS vs Linux)? Max path `..` components allowed? UTF-8 NFC vs NFD normalization (macOS vs
length? This is a security issue (path traversal) and a cross-platform Linux)? Max path length? This is a security issue (path traversal) and a
compatibility issue. What rules should the spec mandate? cross-platform compatibility issue. What rules should the spec mandate?
> _answer:_ > _answer:_
**5. Should we add a version byte after the magic?** Currently `ZNAVSRFG` is **5. Should we add a version byte after the magic?** Currently
followed immediately by protobuf. Adding a version byte (`ZNAVSRFG\x01`) would `ZNAVSRFG` is followed immediately by protobuf. Adding a version byte
allow future framing changes without requiring protobuf parsing to detect the (`ZNAVSRFG\x01`) would allow future framing changes without requiring
version. `MFFileOuter.Version` serves this purpose but requires successful protobuf parsing to detect the version. `MFFileOuter.Version` serves
deserialization to read. Worth the extra byte? this purpose but requires successful deserialization to read. Worth the
extra byte?
> _answer:_ > _answer:_
**6. Should we add a length-prefix after the magic?** Protobuf is not **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 self-delimiting. If we ever want to concatenate manifests or append data
the protobuf, the current framing is insufficient. Add a varint or fixed-width after the protobuf, the current framing is insufficient. Add a varint or
length-prefix? fixed-width length-prefix?
> _answer:_ > _answer:_
### 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
The code currently hashes compressed data (good for verifying before data?** The code currently hashes compressed data (good for verifying
decompression), but this should be explicitly documented. Which is the intended before decompression), but this should be explicitly documented. Which is
behavior? the intended behavior?
> _answer:_ > _answer:_
**8. Should `signatureString()` sign raw bytes instead of a hex-encoded **8. Should `signatureString()` sign raw bytes instead of a hex-encoded
string?** Currently the canonical string is `MAGIC-UUID-MULTIHASH` with hex string?** Currently the canonical string is `MAGIC-UUID-MULTIHASH` with
encoding, which adds a transformation layer. Signing the raw `sha256` bytes (or hex encoding, which adds a transformation layer. Signing the raw `sha256`
compressed `innerMessage` directly) would be simpler. Keep the string format or bytes (or compressed `innerMessage` directly) would be simpler. Keep the
switch to raw bytes? string format or switch to raw bytes?
> _answer:_ > _answer:_
**9. Should we support detached signature files (`.mf.sig`)?** Embedded **9. Should we support detached signature files (`.mf.sig`)?** Embedded
signatures are better for single-file distribution. Detached `.mf.sig` files signatures are better for single-file distribution. Detached `.mf.sig`
follow the familiar `SHASUMS`/`SHASUMS.asc` pattern and are simpler for HTTP files follow the familiar `SHASUMS`/`SHASUMS.asc` pattern and are
serving. Support both modes? simpler for HTTP serving. Support both modes?
> _answer:_ > _answer:_
**10. GPG vs pure-Go crypto for signatures?** Shelling out to `gpg` is fragile **10. GPG vs pure-Go crypto for signatures?** Shelling out to `gpg` is
(may not be installed, version-dependent output). fragile (may not be installed, version-dependent output).
`github.com/ProtonMail/go-crypto` provides pure-Go OpenPGP, or we could use `github.com/ProtonMail/go-crypto` provides pure-Go OpenPGP, or we could
Ed25519/signify (simpler, no key management). Which direction? use Ed25519/signify (simpler, no key management). Which direction?
> _answer:_ > _answer:_
### Implementation Design ### Implementation Design
**11. Should manifests be deterministic by default?** This means: sort file **11. Should manifests be deterministic by default?** This means: sort
entries by path, omit `createdAt` timestamp (or make it opt-in), no `atime`. file entries by path, omit `createdAt` timestamp (or make it opt-in), no
Should determinism be the default, with a `--include-timestamps` flag to opt in? `atime`. Should determinism be the default, with a
`--include-timestamps` flag to opt in?
> _answer:_ > _answer:_
**12. Should we consolidate or keep both scanner/checker implementations?** **12. Should we consolidate or keep both scanner/checker
There are two parallel implementations: `mfer/scanner.go` + `mfer/checker.go` implementations?** There are two parallel implementations:
(typed with `FileSize`, `RelFilePath`) and `internal/scanner/` + `mfer/scanner.go` + `mfer/checker.go` (typed with `FileSize`,
`internal/checker/` (raw `int64`, `string`). The `mfer/` versions are superior. `RelFilePath`) and `internal/scanner/` + `internal/checker/` (raw
Delete the `internal/` versions? `int64`, `string`). The `mfer/` versions are superior. Delete the
`internal/` versions?
> _answer:_ > _answer:_
**13. Should the `manifest` type be exported?** Currently unexported with **13. Should the `manifest` type be exported?** Currently unexported with
exported constructors (`NewManifestFromReader`, `NewManifestFromFile`). exported constructors (`NewManifestFromReader`, `NewManifestFromFile`).
Consumers can't declare `var m *mfer.manifest`. Export the type, or define an Consumers can't declare `var m *mfer.manifest`. Export the type, or
interface? define an interface?
> _answer:_ > _answer:_
**14. What should the Go module path be for 1.0?** Currently **14. What should the Go module path be for 1.0?** Currently
`sneak.berlin/go/mfer` in `go.mod` but `git.eeqj.de/sneak/mfer/mfer` in the `sneak.berlin/go/mfer` in `go.mod` but `git.eeqj.de/sneak/mfer/mfer` in
proto `go_package` option. Which is canonical? the proto `go_package` option. Which is canonical?
> _answer:_ > _answer:_
@@ -373,37 +375,39 @@ proto `go_package` option. Which is canonical?
- [ ] Resolve proto `go_package` path inconsistency - [ ] Resolve proto `go_package` path inconsistency
(`git.eeqj.de/sneak/mfer/mfer` vs `sneak.berlin/go/mfer`) (`git.eeqj.de/sneak/mfer/mfer` vs `sneak.berlin/go/mfer`)
- [ ] Specify path invariants — add proto comments requiring UTF-8, - [ ] Specify path invariants — add proto comments requiring UTF-8,
forward-slash, relative paths, no `..`, no leading `/`; validate in forward-slash, relative paths, no `..`, no leading `/`; validate
`Builder.AddFile` and `Builder.AddFileWithHash` (pending design question in `Builder.AddFile` and `Builder.AddFileWithHash` (pending design
question answer)
- [ ] Remove or deprecate `atime` from proto (pending design question
answer) answer)
- [ ] Remove or deprecate `atime` from proto (pending design question answer) - [ ] Reserve `optional uint32 mode = 305` in `MFFilePath` for future
- [ ] Reserve `optional uint32 mode = 305` in `MFFilePath` for future file file permissions (pending design question answer)
permissions (pending design question answer) - [ ] Add version byte after magic — `ZNAVSRFG\x01` for format version
- [ ] Add version byte after magic — `ZNAVSRFG\x01` for format version 1 1 (pending design question answer)
(pending design question answer) - [ ] Write format specification document — separate from README:
- [ ] Write format specification document — separate from README: magic, outer magic, outer structure, compression, inner structure, path
structure, compression, inner structure, path invariants, signature invariants, signature scheme, canonical serialization
scheme, canonical serialization
### Library ### Library
- [ ] Delete `internal/scanner/` and `internal/checker/` — consolidate on - [ ] Delete `internal/scanner/` and `internal/checker/` — consolidate
`mfer/` package versions; update CLI code (pending design question answer) on `mfer/` package versions; update CLI code (pending design
- [ ] Add deterministic file ordering — sort entries by path (lexicographic, question answer)
byte-order) in `Builder.Build()`; add test asserting byte-identical output - [ ] Add deterministic file ordering — sort entries by path
from two runs (lexicographic, byte-order) in `Builder.Build()`; add test
- [ ] Add decompression size limit — `io.LimitReader` in `deserializeInner()` asserting byte-identical output from two runs
with `m.pbOuter.Size` as bound - [ ] Add decompression size limit — `io.LimitReader` in
- [ ] Fix `errors.Is` dead code in checker — replace with `os.IsNotExist(err)` `deserializeInner()` with `m.pbOuter.Size` as bound
or `errors.Is(err, fs.ErrNotExist)` - [ ] Fix `errors.Is` dead code in checker — replace with
- [ ] Fix `AddFile` to verify size — check `totalRead == size` after reading, `os.IsNotExist(err)` or `errors.Is(err, fs.ErrNotExist)`
return error on mismatch - [ ] Fix `AddFile` to verify size — check `totalRead == size` after
- [ ] Export the `manifest` type or define a public interface (pending design reading, return error on mismatch
question answer) — currently consumers cannot hold a reference to a loaded - [ ] Export the `manifest` type or define a public interface (pending
manifest in their own type declarations design question answer) — currently consumers cannot hold a reference
- [ ] Replace GPG subprocess calls with pure-Go crypto (pending design question to a loaded manifest in their own type declarations
answer) — current implementation shells out to `gpg` which may not be - [ ] Replace GPG subprocess calls with pure-Go crypto (pending design
installed question answer) — current implementation shells out to `gpg` which
may not be installed
- [ ] Add timeout to any remaining subprocess calls - [ ] Add timeout to any remaining subprocess calls
### CLI ### CLI
@@ -412,33 +416,35 @@ proto `go_package` option. Which is canonical?
(`--include-dotfiles`, `--follow-symlinks`) (`--include-dotfiles`, `--follow-symlinks`)
- [ ] Fix URL construction in fetch — use `BaseURL.JoinPath()` or - [ ] Fix URL construction in fetch — use `BaseURL.JoinPath()` or
`url.JoinPath()` instead of string concatenation `url.JoinPath()` instead of string concatenation
- [ ] Add progress rate-limiting to Checker — throttle to once per second, - [ ] Add progress rate-limiting to Checker — throttle to once per
matching Scanner second, matching Scanner
- [ ] Add `--deterministic` flag or make it default — omit `createdAt`, sort - [ ] Add `--deterministic` flag or make it default — omit `createdAt`,
files (pending design question answer) sort files (pending design question answer)
- [ ] Wire `--version` flag properly (currently only a `version` subcommand - [ ] Wire `--version` flag properly (currently only a `version`
exists; top-level `--version` shows urfave/cli generic output) subcommand exists; top-level `--version` shows urfave/cli generic
- [ ] Add retry logic to `fetch` — currently no retries on transient HTTP output)
errors; needs exponential backoff - [ ] Add retry logic to `fetch` — currently no retries on transient
- [ ] `fetch` command uses bare `http.Get` with no timeout — needs `http.Client` HTTP errors; needs exponential backoff
with configurable timeout - [ ] `fetch` command uses bare `http.Get` with no timeout — needs
`http.Client` with configurable timeout
### Testing & Robustness ### Testing & Robustness
- [ ] Add fuzzing tests for `NewManifestFromReader` — protobuf deserialization - [ ] Add fuzzing tests for `NewManifestFromReader` — protobuf
of untrusted input needs fuzz coverage deserialization of untrusted input needs fuzz coverage
- [ ] Add integration test for `freshen` CLI command — current tests only verify - [ ] Add integration test for `freshen` CLI command — current tests
setup, not the actual freshen operation end-to-end only verify setup, not the actual freshen operation end-to-end
- [ ] Add test for `fetch` CLI command end-to-end (currently only `downloadFile` - [ ] Add test for `fetch` CLI command end-to-end (currently only
is tested) `downloadFile` is tested)
### Documentation ### Documentation
- [ ] Promote `FORMAT.md` as primary spec reference; README should link to it - [ ] Promote `FORMAT.md` as primary spec reference; README should link
more prominently to it more prominently
- [ ] Audit and update all error messages for consistency and helpfulness - [ ] Audit and update all error messages for consistency and
- [ ] Document the signature scheme more thoroughly (canonical string format, helpfulness
verification steps) - [ ] Document the signature scheme more thoroughly (canonical string
format, verification steps)
### Release ### Release
@@ -459,8 +465,7 @@ proto `go_package` option. Which is canonical?
## 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)
- Issues: - Issues: [https://git.eeqj.de/sneak/mfer/issues](https://git.eeqj.de/sneak/mfer/issues)
[https://git.eeqj.de/sneak/mfer/issues](https://git.eeqj.de/sneak/mfer/issues)
# Authors # Authors

148
TODO.md
View File

@@ -10,100 +10,102 @@
# Status # Status
pre-1.0. No git tags. README section "TODO: Remaining Work for 1.0" lists open pre-1.0. No git tags. README section "TODO: Remaining Work for 1.0" lists
design questions and implementation tasks; policy compliance work is in flight open design questions and implementation tasks; policy compliance work is
and unmerged. in flight and unmerged.
# Next Step # Next Step
Work through the remaining compliance items folded from the 2026-07-02 audit Work through the remaining compliance items folded from the 2026-07-02
(the first group under Future Steps): `.editorconfig`, `.gitignore` coverage, audit (the first group under Future Steps): `.editorconfig`, `.gitignore`
gofumpt-based `fmt-check`, README "Getting Started", and the rest. coverage, gofumpt-based `fmt-check`, README "Getting Started", and the
`.golangci.yml` and `TODO.md` are tracked and committed as of 2026-08-07, so the rest. `.golangci.yml` and `TODO.md` are tracked and committed as of
only thing left of the `chore/align-repo-policies` branch is the list below. 2026-08-07, so the only thing left of the `chore/align-repo-policies`
branch is the list below.
# Completed Steps # Completed Steps
- 2026-08-09: added `.prettierrc`/`.prettierignore`, gave `script/fmt` and
`script/fmt-check` one shared prettier file set via `script/prettier`, dropped
the `|| true` that hid prettier failures, and added a node-based Dockerfile
stage so a markdown formatting violation fails `docker build .` (#69)
- 2026-08-07: updated golangci-lint to v2.12.2 everywhere it is pinned - 2026-08-07: updated golangci-lint to v2.12.2 everywhere it is pinned
(`Makefile`, `Dockerfile`), added the canonical `.golangci.yml` (`Makefile`, `Dockerfile`), added the canonical `.golangci.yml`
(`default: all`), and fixed all resulting lint findings across the codebase (`default: all`), and fixed all resulting lint findings across the
- 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints, Makefile codebase
shims, README Entrypoints section - 2026-07-07 Adopted scripts-to-rule-them-all: `script/` entrypoints,
- 2026-07-03: aligned repo tooling, docs, and config with standardized policies Makefile shims, README Entrypoints section
(7d9a138, on chore/align-repo-policies, unmerged) - 2026-07-03: aligned repo tooling, docs, and config with standardized
policies (7d9a138, on chore/align-repo-policies, unmerged)
- 2026-06-28: moved to standardized repo policies (#56, on main) - 2026-06-28: moved to standardized repo policies (#56, on main)
- 2026-04-07: added 1.0 roadmap as README TODO section, removed old TODO.md - 2026-04-07: added 1.0 roadmap as README TODO section, removed old
(#54) TODO.md (#54)
- 2026-03-20: added Gitea Actions CI workflow (#53) - 2026-03-20: added Gitea Actions CI workflow (#53)
- 2026-03-17: added REPO_POLICIES.md, renamed CLAUDE.md to AGENTS.md (#51); - 2026-03-17: added REPO_POLICIES.md, renamed CLAUDE.md to AGENTS.md (#51);
removed committed .index.mf (#52) removed committed .index.mf (#52)
- 2026-03-15: split Dockerfile with pre-built golangci-lint stage for faster CI - 2026-03-15: split Dockerfile with pre-built golangci-lint stage for
(#45) faster CI (#45)
- 2026-03-01: 1.0 quality polish: code review, tests, bug fixes, docs (#32) - 2026-03-01: 1.0 quality polish: code review, tests, bug fixes, docs (#32)
- 2026-02-20: deterministic file ordering in Builder.Build() (#28); removed - 2026-02-20: deterministic file ordering in Builder.Build() (#28);
committed vendor/modcache archives (#35) removed committed vendor/modcache archives (#35)
- 2026-02-08: added --seed flag for deterministic manifest UUID - 2026-02-08: added --seed flag for deterministic manifest UUID
# Future Steps # Future Steps
- Compliance (fold of TODO.md audit 2026-07-02; verify which items the in-flight - Compliance (fold of TODO.md audit 2026-07-02; verify which items the
branch already closes, then check off): in-flight branch already closes, then check off):
- Add .editorconfig (canonical copy from sneak/prompts) - Add .editorconfig (canonical copy from sneak/prompts)
- Make .gitignore cover secrets (.env, _.key, _.pem), OS files (.DS_Store), - Make .gitignore cover secrets (.env, _.key, _.pem), OS files
and editor files (_.swp, _~) (.DS_Store), and editor files (_.swp, _~)
- Make fmt-check/lint verify with gofumpt, not gofmt -l, so `make check` - Make fmt-check/lint verify with gofumpt, not gofmt -l, so
matches what `make fmt` writes `make check` matches what `make fmt` writes
- Add README "Getting Started" section with copy-pasteable install/usage - Add README "Getting Started" section with copy-pasteable
block install/usage block
- 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
- Pin Makefile-installed Go tools (`protoc-gen-go@v1.28.1`, reference
`golangci-lint@v2.12.2`) by module hash, not mutable tag - Pin Makefile-installed Go tools (`protoc-gen-go@v1.28.1`,
- Set `make test` timeout to 30s (currently 10s) `golangci-lint@v2.12.2`) by module hash, not mutable tag
- Add explicit README "Rationale" heading (content exists under other - Set `make test` timeout to 30s (currently 10s)
names); name the author in the README Description first line - Add explicit README "Rationale" heading (content exists under other
- Reconcile root-level AGENTS.md with directory-hygiene policy (keep or names); name the author in the README Description first line
relocate) - Reconcile root-level AGENTS.md with directory-hygiene policy (keep
- Add a `make build` target or relocate)
- Rewrite `make hooks` to use printf or a heredoc instead of non-portable - Add a `make build` target
`echo '...\n...'` - Rewrite `make hooks` to use printf or a heredoc instead of
non-portable `echo '...\n...'`
- Answer the 14 owner design questions in the README 1.0 roadmap: - Answer the 14 owner design questions in the README 1.0 roadmap:
- Format: simplify MFFileChecksum; store file mode; drop atime; specify path - Format: simplify MFFileChecksum; store file mode; drop atime;
normalization rules; version byte after magic; length-prefix after magic specify path normalization rules; version byte after magic;
- Signatures: hash covers compressed or uncompressed data; sign raw bytes vs length-prefix after magic
hex canonical string; detached .mf.sig support; GPG subprocess vs pure-Go - Signatures: hash covers compressed or uncompressed data; sign raw
crypto bytes vs hex canonical string; detached .mf.sig support; GPG
- Implementation: deterministic manifests by default; consolidate duplicate subprocess vs pure-Go crypto
scanner/checker implementations; export the manifest type; canonical Go - Implementation: deterministic manifests by default; consolidate
module path for 1.0 duplicate scanner/checker implementations; export the manifest
type; canonical Go module path for 1.0
- Format and correctness: - Format and correctness:
- Resolve proto go_package vs go.mod module path inconsistency - Resolve proto go_package vs go.mod module path inconsistency
- Specify and validate path invariants (UTF-8, forward-slash, relative, no - Specify and validate path invariants (UTF-8, forward-slash,
.., no leading /) relative, no .., no leading /)
- Remove or deprecate atime; reserve mode field; add version byte (all - Remove or deprecate atime; reserve mode field; add version byte
pending design answers) (all pending design answers)
- Write a standalone format specification document - Write a standalone format specification document
- Library: - Library:
- Delete internal/scanner and internal/checker; consolidate on the mfer/ - Delete internal/scanner and internal/checker; consolidate on the
package versions (pending design answer) mfer/ package versions (pending design answer)
- Add decompression size limit via io.LimitReader in deserializeInner() - Add decompression size limit via io.LimitReader in
- Fix errors.Is dead code in checker; make AddFile verify totalRead == size deserializeInner()
- Export manifest type or define a public interface (pending) - Fix errors.Is dead code in checker; make AddFile verify
- Replace GPG subprocess with pure-Go crypto (pending); add timeouts to totalRead == size
remaining subprocess calls - Export manifest type or define a public interface (pending)
- Replace GPG subprocess with pure-Go crypto (pending); add timeouts
to remaining subprocess calls
- CLI: - CLI:
- Kebab-case primary flag names; fix fetch URL construction with - Kebab-case primary flag names; fix fetch URL construction with
url.JoinPath; add http.Client timeout and retry with backoff to fetch; url.JoinPath; add http.Client timeout and retry with backoff to
rate-limit Checker progress output; add --deterministic flag or default; fetch; rate-limit Checker progress output; add --deterministic
wire top-level --version properly flag or default; wire top-level --version properly
- Testing: - Testing:
- Fuzz NewManifestFromReader; 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
document the signature scheme fully messages; document the signature scheme fully
- Release: - Release:
- Finalize module path, bump version constant, SemVer --version output, tag - Finalize module path, bump version constant, SemVer --version
v1.0.0 output, tag v1.0.0

View File

@@ -1,10 +0,0 @@
{
"name": "mfer",
"version": "0.1.0",
"private": true,
"description": "Development tooling for the mfer repository: prettier, used by script/fmt and script/fmt-check to format and verify Markdown and JSON.",
"license": "WTFPL",
"devDependencies": {
"prettier": "3.9.6"
}
}

View File

@@ -130,13 +130,9 @@ main() {
if missing make; then pkg_install gnumake make make make; fi if missing make; then pkg_install gnumake make make make; fi
# ---- JS / docs repos ---- # ---- JS / docs repos ----
# This is a Go repo, but node and yarn are required anyway: prettier # ensure_node
# formats the Markdown and JSON, and script/fmt-check verifies it. # ensure_yarn
# The version is pinned by package.json/yarn.lock, whose integrity # install_js_deps
# hashes --frozen-lockfile enforces.
ensure_node
ensure_yarn
install_js_deps
# ---- Go repos ---- # ---- Go repos ----
if missing go; then pkg_install go golang go go; fi if missing go; then pkg_install go golang go go; fi

View File

@@ -2,8 +2,7 @@
# script/fmt: format all files (writes). # script/fmt: format all files (writes).
set -eu set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
# Regenerate mfer/mf.pb.go from mfer/mf.proto if it is missing or stale # Regenerate mfer/mf.pb.go from mfer/mf.proto if it is missing or stale
# (mirrors the old Makefile prerequisite; the generated file is # (mirrors the old Makefile prerequisite; the generated file is
@@ -20,8 +19,9 @@ main() {
ensure_pb ensure_pb
gofumpt -l -w mfer internal cmd gofumpt -l -w mfer internal cmd
golangci-lint run --fix golangci-lint run --fix
# Markdown and JSON, over the same file set script/fmt-check verifies. # prettier is best-effort, as in the old Makefile (- prefix)
"$SCRIPT_DIR/prettier" --write prettier -w *.json || true
prettier -w *.md || true
} }
main "$@" main "$@"

View File

@@ -1,14 +1,28 @@
#!/bin/sh #!/bin/sh
# script/fmt-check: check formatting (read-only). Same scope as # script/fmt-check: check formatting (read-only). Same scope as
# script/fmt, but fails instead of writing: Go via script/fmt-check-go, # script/fmt, but fails instead of writing.
# Markdown and JSON via script/prettier.
set -eu set -eu
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)" ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Regenerate mfer/mf.pb.go from mfer/mf.proto if it is missing or stale
# (mirrors the old Makefile prerequisite; the generated file is
# committed, so this is normally a no-op).
ensure_pb() {
if [ ! -f mfer/mf.pb.go ] ||
[ -n "$(find mfer/mf.proto -newer mfer/mf.pb.go 2>/dev/null)" ]; then
(cd mfer && go generate .)
fi
}
main() { main() {
"$SCRIPT_DIR/fmt-check-go" cd "$ROOT"
"$SCRIPT_DIR/prettier" --check ensure_pb
if [ -n "$(gofmt -l .)" ]; then
echo "gofmt: files need formatting:" >&2
gofmt -l . >&2
exit 1
fi
} }
main "$@" main "$@"

View File

@@ -1,29 +0,0 @@
#!/bin/sh
# script/fmt-check-go: check Go formatting (read-only). Split out from
# script/fmt-check so the Docker lint stage, whose image has no node and
# therefore no prettier, can run the Go half on its own.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
# Regenerate mfer/mf.pb.go from mfer/mf.proto if it is missing or stale
# (mirrors the old Makefile prerequisite; the generated file is
# committed, so this is normally a no-op).
ensure_pb() {
if [ ! -f mfer/mf.pb.go ] ||
[ -n "$(find mfer/mf.proto -newer mfer/mf.pb.go 2>/dev/null)" ]; then
(cd mfer && go generate .)
fi
}
main() {
cd "$ROOT"
ensure_pb
if [ -n "$(gofmt -l .)" ]; then
echo "gofmt: files need formatting:" >&2
gofmt -l . >&2
exit 1
fi
}
main "$@"

View File

@@ -1,69 +0,0 @@
#!/bin/sh
# script/prettier: run prettier over this repo's canonical file set.
#
# Takes exactly one mode argument, --write or --check, and applies the
# same patterns in both modes. script/fmt and script/fmt-check both go
# through here, so the set of files that get formatted and the set that
# get verified cannot drift apart.
#
# Failures are never swallowed: a missing prettier is an error, not a
# silent skip. A formatter that quietly does nothing is worse than one
# that fails loudly.
set -eu
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
usage() {
echo "usage: script/prettier --write|--check" >&2
exit 2
}
# Prefer the version pinned by package.json/yarn.lock so that CI and
# developer machines format identically. Fall back to a prettier on PATH,
# but say so, because a different version formats differently.
find_prettier() {
if [ -x "$ROOT/node_modules/.bin/prettier" ]; then
printf '%s\n' "$ROOT/node_modules/.bin/prettier"
return 0
fi
if command -v prettier >/dev/null 2>&1; then
echo "prettier: node_modules/.bin/prettier is absent; using the" \
"prettier on PATH, which may be a different version than the" \
"one pinned in package.json. Run script/bootstrap to install" \
"the pinned version." >&2
command -v prettier
return 0
fi
return 1
}
main() {
[ "$#" -eq 1 ] || usage
case "$1" in
--write | --check) mode="$1" ;;
*) usage ;;
esac
cd "$ROOT"
if ! prettier_bin="$(find_prettier)"; then
echo "prettier: not found." >&2
echo " Install it with: script/bootstrap" >&2
echo " (installs the version pinned in package.json/yarn.lock)" >&2
exit 1
fi
# Markdown and JSON, repo-wide rather than root-only, so files in
# subdirectories (docs/, once it exists) are covered too. Exclusions
# live in .prettierignore; REPO_POLICIES.md is excluded there because
# it is a verbatim copy of an upstream document.
#
# --no-error-on-unmatched-pattern is deliberately NOT used: both
# patterns always match at least one tracked file (README.md,
# package.json), so an empty match means the glob broke, and prettier
# erroring out is exactly what we want rather than a vacuous pass.
"$prettier_bin" "$mode" "**/*.md"
"$prettier_bin" "$mode" "**/*.json"
}
main "$@"

View File

@@ -1,8 +0,0 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
prettier@3.9.6:
version "3.9.6"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.9.6.tgz#b3ea5146515d40fc53f18aa63f74dfab1e10dbf6"
integrity sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==