publish() opened the destination stream outside its guarded region, justified by "nothing
has been written at that point, so there is nothing of ours to remove". That reasoning is
wrong about what exists: SAF's CreateDocument contract creates the document before
publish() is ever called -- which is why every fixture in OutputPublisherPublishTest
starts as an existing empty file. A provider that then hands out no stream, because it
dropped between the picker and the write or simply returns null, left a zero-byte file at
the name the user chose while the screen said the save had failed.
The open moves inside the try, so the same two bounds that already govern a failed copy
govern this: only a document URI, and only a destination positively known to be empty. The
dead-provider case is untouched and now demonstrably by the guard rather than by the
placement -- nothing answers for that authority, so no size can be read, and "I could not
tell" still refuses to authorise a delete. Its test comment said the old thing and now says
that one.
The space arithmetic overflows in two places, both live on main and independent of the
allocatable-versus-usable question that stays parked:
- hasSpaceFor computed `free > required + headroom`. A request within 128 MiB of
Long.MAX_VALUE wraps that sum negative, and every free-space measurement beats a
negative number, so the check answers "plenty of room" to the largest request it can be
handed. Rewritten as `free - headroom > required` with both operands clamped at zero,
which is the form the parked branch's StagingSpace.hasRoomFor already argues for.
- InputQuery.total folded a join's inputs with nothing stopping the sum from wrapping,
and that is the reachable half: no single file overflows, three four-exabyte inputs do.
It saturates at Long.MAX_VALUE now, which the check above then refuses.
SpaceArithmeticTest ties the two together in the shape the defect had -- the total that
came out negative is handed straight to the space check -- and keeps one allowed case so
the refusals cannot pass by refusing everything. The negative-size clamp is deliberately
left unasserted, with a comment saying why: it only changes the answer when free space is
below the headroom, which a test reading the host's real cache volume cannot arrange.
R6 / #15, R23 / #32
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>