# zit 0.3.24

zit 0.3.24 closes a door in branch protection: a push may no longer close,
or reopen, a protected branch it is not allowed to commit to. It also
fixes a crash in the container decoder that any peer could trigger with a
damaged sync body, and makes that decoder verify the checksum it read.

No artifact, tag namespace, sync message, or on-disk repository format
changed. One more shape of pushed tag is refused by a server that
protects branches; every other push is admitted exactly as before.

## Closing a protected branch is a protected move

A server given protected-branch globs refuses a pushed check-in that would
join one of those branches, and a pushed tag that would move a held
check-in into or out of one: `sym-B` under any sign, and `branch=B` by
value. Both name the branch in the tag itself, so the gate judged tags by
what they named.

A `closed` tag names a check-in, not a branch. The gate let it through, and
its test said so on purpose: closing a leaf is not a membership change.
That reasoning was right and the rule was still half a rule. An account
with check-in rights but no ownership of `trunk`'s protection could not
push onto trunk, and could close its tip. The owners' next commit was then
refused against a closed leaf, with a message that named neither the cause
nor the actor, and the recovery was a raw tag cancel that appears in no
interface. The cheapest way past a protected branch was not to push to it.

The gate now treats lifecycle as it treats membership. A `closed` tag aimed
at a check-in this server holds resolves that check-in's branch through the
resolver the gate already has, and is refused when the branch is protected,
under any sign: `+closed` and `*closed` would close it, `-closed` would
reopen it, and the refusal says which.

The line is drawn narrowly. A release name on a protected leaf is admitted.
Closing a leaf off the protected branch is admitted. A `closed` aimed at a
check-in this server does not hold is admitted, as an unknown parent is: it
cannot be judged yet and its target is judged on arrival. A check-in that
closes itself has no target and is judged by where it joins. A resolver
that cannot answer refuses, as it always did; a protection that admits
under a read fault is not one.

## A damaged container is an error, not a crash and not wrong bytes

The first full nightly soak, added in the previous release, found this on
its first night. The fuzz target that feeds the container codec damaged
containers took a valid container, flipped one bit, and brought the process
down: an integer underflow inside the standard library's inflate, in its
end-of-input bit peek, that a truncated stream can arrange. Every sync
request and reply passes through that decoder, so any peer could send
those bytes to a server or a client.

The decoder is now the tree's own copy of the standard one, with the
comparison rearranged so the stream ends instead of underflowing, and a
note on where it came from and why; it goes back to the standard library
when the fix is upstream. With the crash gone, the same bytes decoded to
fifty-eight bytes reported as good: the flip had made a block final, the
stream ended cleanly early, and the zlib checksum in the footer was read
but never checked. The codec now requires a finished stream and verifies
the footer's Adler-32 against the output. A container that lies about
either is refused. The exact bytes are a fixed test.

## Compatibility

- No on-disk repository or wire format changed; Fossil RFC revision 00
  remains the target.
- A server with no protected globs is unchanged. On a server with them, the
  only new refusal is a `closed` tag, under any sign, whose held target is
  on a protected branch. The refusal is a protocol error card the pusher
  reads, naming the tag, the target, and the branch.

## Qualification

`zig fmt --check`, `zig build test`, `e2e`, `smoke`, `muscle`,
`git-import-e2e`, `git-export-e2e`, `dst`, and `cross`, plus the
three-runner CI battery.
